import type { Meta, StoryObj } from "@storybook/angular"; import { moduleMetadata } from "@storybook/angular"; import { CommonModule } from "@angular/common"; import { Component, Injectable, signal } from "@angular/core"; import { FormsModule } from "@angular/forms"; import { CopilotChatView, CopilotChatMessageView, CopilotChatInput, ChatState, provideCopilotChatLabels, provideCopilotKit, } from "@copilotkit/angular"; import type { Message } from "@ag-ui/client"; import { CustomDisclaimerComponent } from "../components/custom-disclaimer.component"; import { CustomInputComponent } from "../components/custom-input.component"; import { CustomScrollButtonComponent } from "../components/custom-scroll-button.component"; @Injectable() class StoryChatState extends ChatState { readonly inputValue = signal(""); submitInput(value: string): void { const trimmed = value.trim(); if (!trimmed) return; console.log("[Storybook] submitInput", trimmed); this.inputValue.set(""); } changeInput(value: string): void { this.inputValue.set(value); } } const meta: Meta = { title: "UI/CopilotChatView/Customized with Components", component: CopilotChatView, decorators: [ moduleMetadata({ imports: [ CommonModule, CopilotChatView, CopilotChatMessageView, CopilotChatInput, ], providers: [ provideCopilotKit({}), provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", chatDisclaimerText: "AI can make mistakes. Please verify important information.", }), { provide: ChatState, useClass: StoryChatState }, ], }), ], parameters: { layout: "fullscreen", }, }; export default meta; type Story = StoryObj; export const CustomDisclaimer: Story = { parameters: { docs: { source: { type: "code", code: `import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CopilotChatView, CopilotChatMessageView, CopilotChatInput, provideCopilotKit, provideCopilotChatLabels } from '@copilotkit/angular'; import { Message } from '@ag-ui/client'; // Custom disclaimer component @Component({ selector: 'custom-disclaimer', standalone: true, template: \`

✨ Custom Disclaimer Component ✨

{{ '{' }}{{ '{' }} text || 'This is a custom disclaimer demonstrating component overrides!' {{ '}' }}{{ '}' }}

🎨 Styled with custom gradients and animations
\` }) class CustomDisclaimerComponent { @Input() text?: string; @Input() inputClass?: string; } @Component({ selector: 'app-custom-disclaimer', standalone: true, imports: [ CommonModule, CopilotChatView, CopilotChatMessageView, CopilotChatInput, CustomDisclaimerComponent ], providers: [ provideCopilotKit({}), provideCopilotChatLabels({ chatInputPlaceholder: "Type a message...", chatDisclaimerText: "AI can make mistakes. Please verify important information.", }) ], template: \`
\` }) export class CustomDisclaimerExampleComponent { messages: Message[] = [ { id: 'user-1', content: 'Hello! Can you help me with TypeScript?', role: 'user' }, { id: 'assistant-1', content: 'Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?', role: 'assistant' } ]; customDisclaimerComponent = CustomDisclaimerComponent; }`, language: "typescript", }, }, }, render: () => { const messages: Message[] = [ { id: "user-1", content: "Hello! Can you help me with TypeScript?", role: "user" as const, }, { id: "assistant-1", content: "Of course! TypeScript is a superset of JavaScript that adds static typing. What would you like to know?", role: "assistant" as const, }, ]; return { template: `
`, props: { messages, customDisclaimerComponent: CustomDisclaimerComponent, }, }; }, }; export const CustomInput: Story = { parameters: { docs: { source: { type: "code", code: `import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CopilotChatView, CopilotChatMessageView, CopilotChatInput, ChatState, provideCopilotKit, provideCopilotChatLabels } from '@copilotkit/angular'; import { Message } from '@ag-ui/client'; // Custom input component @Component({ selector: 'custom-input', standalone: true, imports: [CommonModule, FormsModule], template: \`
\` }) class CustomInputComponent { @Input() inputClass?: string; inputValue = ''; constructor(private chat: ChatState) {} handleSend() { const value = this.inputValue.trim(); if (value) { this.chat.submitInput(value); this.inputValue = ''; } } } @Component({ selector: 'app-custom-input', standalone: true, imports: [ CommonModule, CopilotChatView, CopilotChatMessageView, CopilotChatInput, CustomInputComponent ], providers: [ provideCopilotKit({}), provideCopilotChatLabels({ chatInputPlaceholder: 'Type a message...', chatDisclaimerText: 'AI can make mistakes. Please verify important information.' }) ], template: \`
\` }) export class CustomInputExampleComponent { messages: Message[] = [ { id: 'user-1', content: 'Check out this custom input!', role: 'user' }, { id: 'assistant-1', content: 'That's a beautiful custom input component! The gradient and styling look great.', role: 'assistant' } ]; customInputComponent = CustomInputComponent; }`, language: "typescript", }, }, }, render: () => { const messages: Message[] = [ { id: "user-1", content: "Check out this custom input!", role: "user" as const, }, { id: "assistant-1", content: "That's a beautiful custom input component! The gradient and styling look great.", role: "assistant" as const, }, ]; return { template: `
`, props: { messages, customInputComponent: CustomInputComponent, }, }; }, }; export const CustomScrollButton: Story = { parameters: { docs: { source: { type: "code", code: `import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CopilotChatView, CopilotChatMessageView, CopilotChatInput, provideCopilotKit, provideCopilotChatLabels } from '@copilotkit/angular'; import { Message } from '@ag-ui/client'; // Custom scroll button component @Component({ selector: 'custom-scroll-button', standalone: true, imports: [CommonModule], template: \` \`, styles: [\` button.hover { transform: scale(1.1); } \`] }) class CustomScrollButtonComponent { @Input() onClick?: () => void; @Input() inputClass?: string; @Output() clicked = new EventEmitter(); isHovered = false; handleClick() { this.clicked.emit(); if (this.onClick) { this.onClick(); } } } @Component({ selector: 'app-custom-scroll', standalone: true, imports: [ CommonModule, CopilotChatView, CopilotChatMessageView, CopilotChatInput, CustomScrollButtonComponent ], providers: [ provideCopilotKit({}), provideCopilotChatLabels({ chatInputPlaceholder: 'Type a message...', chatDisclaimerText: 'AI can make mistakes. Please verify important information.' }) ], template: \`
\` }) export class CustomScrollButtonExampleComponent { messages: Message[] = []; scrollToBottomButtonComponent = CustomScrollButtonComponent; constructor() { // Generate many messages to show scroll behavior for (let i = 0; i < 20; i++) { this.messages.push({ id: \`msg-\${i}\`, content: \`Message \${i}: This is a test message to demonstrate the custom scroll button.\`, role: i % 2 === 0 ? 'user' : 'assistant' } as Message); } } }`, language: "typescript", }, }, }, render: () => { // Generate many messages to show scroll behavior const messages: Message[] = []; for (let i = 0; i < 20; i++) { messages.push({ id: `msg-${i}`, content: `Message ${i}: This is a test message to demonstrate the custom scroll button.`, role: i % 2 === 0 ? "user" : "assistant", } as Message); } return { template: `
`, props: { messages, scrollToBottomButtonComponent: CustomScrollButtonComponent, }, }; }, }; export const NoFeatherEffect: Story = { parameters: { docs: { source: { type: "code", code: `import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CopilotChatView, CopilotChatMessageView, CopilotChatInput, provideCopilotKit, provideCopilotChatLabels } from '@copilotkit/angular'; import { Message } from '@ag-ui/client'; @Component({ selector: 'app-no-feather', standalone: true, imports: [ CommonModule, CopilotChatView, CopilotChatMessageView, CopilotChatInput ], providers: [ provideCopilotKit({}), provideCopilotChatLabels({ chatInputPlaceholder: 'Type a message...', chatDisclaimerText: 'AI can make mistakes. Please verify important information.' }) ], template: \`
\` }) export class NoFeatherEffectComponent { messages: Message[] = [ { id: 'user-1', content: 'Hello!', role: 'user' }, { id: 'assistant-1', content: 'Hi there! How can I help you today?', role: 'assistant' } ]; }`, language: "typescript", }, }, }, render: () => { const messages: Message[] = [ { id: "user-1", content: "Hello!", role: "user" as const, }, { id: "assistant-1", content: "Hi there! How can I help you today?", role: "assistant" as const, }, ]; return { template: `
`, props: { messages, }, }; }, }; export const CustomInputServiceBased: Story = { name: "Custom Input via Service (Recommended)", parameters: { docs: { description: { story: ` Demonstrates the recommended approach for custom inputs using service injection. This pattern uses \`ChatState.submitInput()\` to submit messages, which is the idiomatic Angular approach for cross-component communication. **Key differences from React:** - Angular uses dependency injection with services - React uses callback props (e.g., \`onSubmitMessage\`) - Both achieve the same result with framework-appropriate patterns `, }, source: { type: "code", code: `import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CopilotChatView, CopilotChatMessageView, CopilotChatInput, ChatState, provideCopilotKit, provideCopilotChatLabels } from '@copilotkit/angular'; import { Message } from '@ag-ui/client'; // Minimal custom input component with service injection @Component({ selector: 'service-based-input', standalone: true, imports: [CommonModule, FormsModule], template: \`

Service-Based Custom Input

This component uses ChatState.submitInput()

\` }) class ServiceBasedInputComponent { value = ''; constructor(private chat: ChatState) {} submit() { const trimmedValue = this.value.trim(); if (!trimmedValue) return; // Use the service to submit the message this.chat.submitInput(trimmedValue); this.value = ''; } } @Component({ selector: 'app-service-based-example', standalone: true, imports: [ CommonModule, CopilotChatView, CopilotChatMessageView, CopilotChatInput, ServiceBasedInputComponent ], providers: [ provideCopilotKit({}), provideCopilotChatLabels({ chatInputPlaceholder: 'Type a message...', chatDisclaimerText: 'AI can make mistakes. Please verify important information.' }) ], template: \`
\` }) export class ServiceBasedExampleComponent { messages: Message[] = [ { id: 'user-1', content: 'How does the service-based approach work?', role: 'user' }, { id: 'assistant-1', content: 'The service-based approach uses Angular\\'s dependency injection to access ChatState, which provides the submitInput() method for sending messages. This is the idiomatic Angular pattern!', role: 'assistant' } ]; customInputComponent = ServiceBasedInputComponent; }`, language: "typescript", }, }, }, render: () => { // Define the service-based input component inline for the story @Component({ selector: "story-service-input", standalone: true, imports: [CommonModule, FormsModule], template: `

Service-Based Custom Input

This component uses ChatState.submitInput()

`, }) class StoryServiceInputComponent { value = ""; constructor(private chat: ChatState) {} submit() { const trimmedValue = this.value.trim(); if (!trimmedValue) return; this.chat.submitInput(trimmedValue); this.value = ""; } } const messages: Message[] = [ { id: "user-1", content: "How does the service-based approach work?", role: "user" as const, }, { id: "assistant-1", content: "The service-based approach uses Angular's dependency injection to access ChatState, which provides the submitInput() method for sending messages. This is the idiomatic Angular pattern!", role: "assistant" as const, }, ]; return { template: `
`, props: { messages, customInputComponent: StoryServiceInputComponent, }, }; }, };