import { Component, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
/**
* Component for entering human responses
*
* @remarks
* This component provides a form for humans to enter their
* response to an MCP request. It includes tool redirect
* buttons and a text input for custom responses.
*
* @example
* ```html
* <app-response-input
* [requestId]="request.id"
* (responseSubmitted)="handleResponse($event)">
* </app-response-input>
* ```
*/
@Component({
selector: 'app-response-input',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="response-input" role="region" aria-label="Response Input Form">
<!-- Bail-out buttons at the top as big buttons -->
<div class="top-bail-out-buttons" role="group" aria-label="Alternative Tool Options">
<button
type="button"
class="big-bail-out-btn"
(click)="bailOutToTool('ask-multiple-choice')"
[disabled]="isSubmitting"
aria-label="Switch to multiple choice format instead of open question"
title="Redirect to structured multiple choice instead">
📝 Use Multiple Choice Instead
</button>
<button
type="button"
class="big-bail-out-btn"
(click)="bailOutToTool('challenge-hypothesis')"
[disabled]="isSubmitting"
aria-label="Switch to hypothesis challenge format instead of open question"
title="Redirect to validate assumptions instead">
🔍 Use Hypothesis Challenge Instead
</button>
</div>
<div class="custom-response" role="group" aria-label="Custom Response Form">
<form (ngSubmit)="submitCustomResponse()" role="form">
<label for="response-textarea" class="sr-only">Custom response text</label>
<textarea
#responseTextarea
id="response-textarea"
[(ngModel)]="customResponse"
name="response"
placeholder="Enter your response..."
rows="3"
[disabled]="isSubmitting"
(keydown)="onKeyDown($event)"
aria-label="Enter your custom response"
aria-describedby="char-count-text"
[attr.aria-invalid]="customResponse.length > 1000">
</textarea>
<div class="form-actions" role="group" aria-label="Form Actions">
<div class="main-actions">
<span
id="char-count-text"
class="char-count"
[class.warning]="customResponse.length > 900"
role="status"
aria-live="polite">
{{ customResponse.length }} / 1000 characters
</span>
<button
type="button"
class="btn-secondary clear-btn"
(click)="clearForm()"
[disabled]="!customResponse.trim() || isSubmitting"
aria-label="Clear the response field"
title="Clear the response field">
Clear
</button>
<button
type="button"
class="btn-icon btn-done"
(click)="submitResponseWithStatus('done')"
[disabled]="!customResponse.trim() || isSubmitting || customResponse.length > 1000"
aria-label="Submit response and indicate completion"
title="Submit and I am done with Answering">
<span aria-hidden="true">✅</span>
<span class="sr-only">Done</span>
</button>
<button
type="button"
class="btn-icon btn-drill-deeper"
(click)="submitResponseWithStatus('drill-deeper')"
[disabled]="!customResponse.trim() || isSubmitting || customResponse.length > 1000"
aria-label="Submit response and request more questions"
title="Submit, drill deeper with more single questions">
<span aria-hidden="true">🔍</span>
<span class="sr-only">Drill Deeper</span>
</button>
<button
type="submit"
class="submit-btn"
[disabled]="!customResponse.trim() || isSubmitting || customResponse.length > 1000"
aria-label="Submit custom response">
{{ isSubmitting ? 'Sending...' : 'Send Response' }}
</button>
</div>
</div>
</form>
</div>
</div>
`,
styles: [`
.response-input {
background: var(--card-bg, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
padding: 1.5rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.top-bail-out-buttons {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 1.5rem;
}
.big-bail-out-btn {
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
color: #856404;
border: 2px solid #ffc107;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 280px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.big-bail-out-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
border-color: #f57c00;
color: #f57c00;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.big-bail-out-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.custom-response form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 4px;
font-family: inherit;
font-size: 0.875rem;
resize: vertical;
transition: border-color 0.2s ease;
}
textarea:focus {
outline: none;
border-color: var(--primary-color, #007bff);
}
textarea:disabled {
background: var(--bg-disabled, #f5f5f5);
cursor: not-allowed;
}
.form-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 1rem;
}
.main-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.char-count {
font-size: 0.75rem;
color: var(--text-secondary, #666);
}
.char-count.warning {
color: var(--danger-color, #dc3545);
}
.submit-btn {
padding: 0.5rem 1.5rem;
background: var(--primary-color, #007bff);
color: white;
border: none;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.submit-btn:hover:not(:disabled) {
background: var(--primary-color-dark, #0056b3);
}
.submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-icon {
padding: 0.5rem;
width: 2.5rem;
height: 2.5rem;
border: none;
border-radius: 50%;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.btn-icon:hover:not(:disabled) {
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-icon.btn-done {
background: #28a745;
color: white;
}
.btn-icon.btn-done:hover:not(:disabled) {
background: #218838;
}
.btn-icon.btn-drill-deeper {
background: #6f42c1;
color: white;
}
.btn-icon.btn-drill-deeper:hover:not(:disabled) {
background: #5a32a3;
}
.btn-icon:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
padding: 0.5rem 1rem;
border: 1px solid #6c757d;
border-radius: 4px;
background: transparent;
color: #6c757d;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.875rem;
}
.btn-secondary:hover:not(:disabled) {
background: #6c757d;
color: white;
}
.btn-secondary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@media (max-width: 480px) {
.top-bail-out-buttons {
flex-direction: column;
gap: 0.75rem;
}
.big-bail-out-btn {
min-width: auto;
font-size: 1rem;
padding: 0.875rem 1.5rem;
}
}
`]
})
export class ResponseInputComponent {
@Input({ required: true }) requestId!: string;
@Input() hasError = false;
@Output() responseSubmitted = new EventEmitter<{ requestId: string; response: string; completionStatus?: 'done' | 'drill-deeper' }>();
@Output() toolRedirect = new EventEmitter<{ requestId: string; toolName: string; message: string }>();
@ViewChild('responseTextarea') textareaRef!: ElementRef<HTMLTextAreaElement>;
customResponse = '';
isSubmitting = false;
submitCustomResponse() {
if (this.customResponse.trim() && this.customResponse.length <= 1000) {
this.submitResponse(this.customResponse.trim());
}
}
submitResponseWithStatus(completionStatus: 'done' | 'drill-deeper') {
if (this.customResponse.trim() && this.customResponse.length <= 1000) {
this.submitResponse(this.customResponse.trim(), completionStatus);
}
}
onKeyDown(event: KeyboardEvent) {
// Submit on Ctrl+Enter or Cmd+Enter
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
this.submitCustomResponse();
}
// Handle Escape key to clear form
if (event.key === 'Escape') {
event.preventDefault();
this.clearForm();
}
}
/**
* Focus the textarea for accessibility
*/
focusTextarea(): void {
if (this.textareaRef) {
this.textareaRef.nativeElement.focus();
}
}
clearForm(): void {
this.customResponse = '';
}
bailOutToTool(toolName: 'ask-multiple-choice' | 'challenge-hypothesis') {
if (this.isSubmitting) return;
const messages = {
'ask-multiple-choice': 'STOP! The human REFUSES to answer this question format and DEMANDS you use the ask-multiple-choice tool instead. Do NOT continue with single questions. IMMEDIATELY reformulate as multiple choice options and call ask-multiple-choice.',
'challenge-hypothesis': 'STOP! The human REFUSES to answer this question format and DEMANDS you use the challenge-hypothesis tool instead. Do NOT continue with single questions. IMMEDIATELY reformulate as hypothesis statements and call challenge-hypothesis.'
};
this.toolRedirect.emit({
requestId: this.requestId,
toolName,
message: messages[toolName]
});
}
private submitResponse(response: string, completionStatus?: 'done' | 'drill-deeper') {
if (this.isSubmitting) return;
this.isSubmitting = true;
const responseData: { requestId: string; response: string; completionStatus?: 'done' | 'drill-deeper' } = {
requestId: this.requestId,
response
};
if (completionStatus) {
responseData.completionStatus = completionStatus;
}
this.responseSubmitted.emit(responseData);
// Reset form after a delay only if no error occurred
// Parent component should clear hasError on successful submission
setTimeout(() => {
if (!this.hasError) {
this.customResponse = '';
}
this.isSubmitting = false;
}, 1000);
}
}