import { Component, signal, computed, inject, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { RequestDisplayComponent } from '../request-display/request-display.component';
import { ResponseInputComponent } from '../response-input/response-input.component';
import { MultipleChoiceResponseComponent } from '../multiple-choice-response/multiple-choice-response.component';
import { HypothesisResponseComponent } from '../hypothesis-response/hypothesis-response.component';
import { ChooseNextResponseComponent } from '../choose-next-response/choose-next-response.component';
import { McpClientService } from '../../services/mcp-client.service';
import { NotificationService } from '../../services/notification.service';
import { QueuedRequest, MultipleChoiceQuestion, HypothesisChallenge, ChooseNextChallenge } from '@ask-me-mcp/askme-shared';
/**
* Simple request handler component
*
* @remarks
* This component handles individual MCP requests as they come in.
* No session or queue management - just direct request/response handling.
*/
@Component({
selector: 'app-request-handler',
standalone: true,
imports: [
CommonModule,
RequestDisplayComponent,
ResponseInputComponent,
MultipleChoiceResponseComponent,
HypothesisResponseComponent,
ChooseNextResponseComponent
],
template: `
<div class="request-handler" role="main" aria-label="MCP Request Handler">
<header class="handler-header" role="banner">
<div class="header-title">
<img src="me.png" alt="Ask Me MCP Logo" class="app-icon">
<h1>Ask Me MCP</h1>
</div>
<div class="connection-status"
[class.connected]="connected()"
role="status"
[attr.aria-label]="connected() ? 'Connected to server' : 'Disconnected from server'">
<span class="status-indicator" aria-hidden="true"></span>
{{ connected() ? 'Connected' : 'Disconnected' }}
</div>
</header>
<main class="handler-content">
<!-- Error message display -->
@if (recentError(); as error) {
<div class="error-message" role="alert" aria-live="assertive">
<h2>❌ Submission Error</h2>
<p>{{ error.message }}</p>
<p class="error-time">Occurred at {{ error.timestamp | date:'short' }}</p>
</div>
}
@if (currentRequest(); as request) {
<div class="current-request">
<app-request-display [request]="request"></app-request-display>
@if (isMultipleChoiceRequest(request)) {
<app-multiple-choice-response
[requestId]="request.id"
[questionsInput]="getMultipleChoiceQuestions(request)"
(responseSubmitted)="onMultipleChoiceResponseSubmitted($event)"
(toolRedirect)="onToolRedirect($event)">
</app-multiple-choice-response>
} @else if (isHypothesisChallengeRequest(request)) {
<app-hypothesis-response
[requestId]="request.id"
[challengeInput]="getHypothesisChallenge(request)"
(responseSubmitted)="onHypothesisChallengeResponseSubmitted($event)"
(toolRedirect)="onToolRedirect($event)">
</app-hypothesis-response>
} @else if (isChooseNextRequest(request)) {
<app-choose-next-response
[requestId]="request.id"
[challengeInput]="getChooseNextChallenge(request)"
(responseSubmitted)="onChooseNextResponseSubmitted($event)"
(toolRedirect)="onToolRedirect($event)">
</app-choose-next-response>
} @else {
<app-response-input
[requestId]="request.id"
[hasError]="!!recentError()"
(responseSubmitted)="onResponseSubmitted($event)"
(toolRedirect)="onToolRedirect($event)">
</app-response-input>
}
</div>
} @else {
<div class="no-request">
@if (recentTimeout(); as timeout) {
<div class="timeout-message">
<h2>⏰ Request Timed Out</h2>
<p>{{ timeout.message }}</p>
<p class="timeout-time">Occurred at {{ timeout.timestamp | date:'short' }}</p>
</div>
} @else {
@if (recentCancellation(); as cancellation) {
<div class="cancellation-message">
<h2>❌ Aborted by Client</h2>
<p>{{ cancellation.message }}</p>
<p class="cancellation-time">Occurred at {{ cancellation.timestamp | date:'short' }}</p>
</div>
} @else {
<div class="waiting-message">
<h2>Waiting for requests...</h2>
<p>MCP clients can send questions using the ask-one-question, ask-multiple-choice, or challenge-hypothesis tools.</p>
<div class="connection-info">
@if (connected()) {
<span class="status-text connected">✓ Connected to server</span>
} @else {
<span class="status-text disconnected">✗ Disconnected from server</span>
}
</div>
</div>
}
}
</div>
}
</main>
</div>
`,
styles: [`
.request-handler {
min-height: 100vh;
display: flex;
flex-direction: column;
background: transparent; /* Show blueprint grid background */
}
.handler-header {
background: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid rgba(64, 164, 223, 0.3);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
}
.header-title {
display: flex;
align-items: center;
gap: 0.75rem;
}
.app-icon {
width: 2rem;
height: 2rem;
object-fit: contain;
border-radius: 4px;
flex-shrink: 0;
}
.handler-header h1 {
margin: 0;
font-size: 1.5rem;
color: var(--text-primary, #333);
}
.connection-status {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary, #666);
font-weight: 500;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--status-disconnected, #dc3545);
}
.connection-status.connected .status-indicator {
background: var(--status-connected, #28a745);
}
.handler-content {
flex: 1;
padding: 2rem;
max-width: 1000px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(1px);
}
.current-request {
display: flex;
flex-direction: column;
gap: 2rem;
}
.no-request {
display: flex;
align-items: center;
justify-content: center;
min-height: 60vh;
}
.waiting-message {
text-align: center;
max-width: 500px;
padding: 3rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
border: 1px solid rgba(64, 164, 223, 0.3);
}
.waiting-message h2 {
margin: 0 0 1rem 0;
color: var(--text-primary, #333);
font-size: 1.5rem;
}
.waiting-message p {
margin: 0 0 2rem 0;
color: var(--text-secondary, #666);
line-height: 1.6;
}
.connection-info {
padding: 1rem;
border-radius: 8px;
background: rgba(248, 249, 250, 0.9);
border: 1px solid rgba(64, 164, 223, 0.2);
}
.status-text {
font-weight: 500;
}
.status-text.connected {
color: #28a745;
}
.status-text.disconnected {
color: #dc3545;
}
.timeout-message {
background: rgba(255, 243, 205, 0.95);
border: 2px solid #ff9800;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
}
.timeout-message h2 {
margin: 0 0 1rem 0;
color: #e65100;
font-size: 1.25rem;
}
.timeout-message p {
margin: 0.5rem 0;
color: #bf360c;
}
.timeout-time {
font-size: 0.9rem;
opacity: 0.8;
font-style: italic;
}
.cancellation-message {
background: rgba(255, 235, 238, 0.95);
border: 2px solid #f44336;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
}
.cancellation-message h2 {
margin: 0 0 1rem 0;
color: #c62828;
font-size: 1.25rem;
}
.cancellation-message p {
margin: 0.5rem 0;
color: #b71c1c;
}
.cancellation-time {
font-size: 0.9rem;
opacity: 0.8;
font-style: italic;
}
.error-message {
background: rgba(255, 235, 238, 0.95);
border: 2px solid #dc3545;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
}
.error-message h2 {
margin: 0 0 1rem 0;
color: #c62828;
font-size: 1.25rem;
}
.error-message p {
margin: 0.5rem 0;
color: #b71c1c;
}
.error-time {
font-size: 0.9rem;
opacity: 0.8;
font-style: italic;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.handler-header {
padding: 1rem;
flex-direction: column;
gap: 1rem;
text-align: center;
}
.handler-content {
padding: 1rem;
}
.waiting-message {
padding: 2rem 1rem;
}
}
`]
})
export class RequestHandlerComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute);
private mcpClient = inject(McpClientService);
private notifications = inject(NotificationService);
// Simple current request signal
currentRequest = signal<QueuedRequest | null>(null);
connected = this.mcpClient.connected;
// Track recent timeout for user feedback
recentTimeout = signal<{ message: string; timestamp: Date } | null>(null);
// Track recent cancellation for user feedback
recentCancellation = signal<{ message: string; timestamp: Date } | null>(null);
// Track submission errors for user feedback
recentError = signal<{ message: string; timestamp: Date } | null>(null);
ngOnInit() {
// Request notification permission
if (this.notifications.isSupported() &&
this.notifications.permission() === 'default') {
this.notifications.requestPermission();
}
// Connect to the server
this.mcpClient.connect('demo'); // Simple fixed session ID
// Listen for new requests
this.mcpClient.requests.subscribe(requests => {
// Just take the first active request if any
const activeRequest = requests.find(r => r.status === 'pending' || r.status === 'active');
// If request was cleared (due to timeout or cancellation), show appropriate message
if (!activeRequest && this.currentRequest()) {
const clearReason = this.mcpClient.lastClearReason();
if (clearReason === 'timeout') {
this.showTimeoutMessage();
} else if (clearReason === 'cancelled') {
this.showCancellationMessage();
}
}
this.currentRequest.set(activeRequest || null);
// Show notification for new requests
if (activeRequest && this.notifications.permission() === 'granted') {
let message = activeRequest.question;
if (activeRequest.type === 'multiple-choice') {
message = 'New multiple choice question';
} else if (activeRequest.type === 'hypothesis-challenge') {
message = 'New hypothesis challenge';
} else if (activeRequest.type === 'choose-next') {
message = 'New decision point';
}
this.notifications.showNotification('New MCP Request', { body: message });
}
});
}
ngOnDestroy() {
this.mcpClient.disconnect();
}
onResponseSubmitted(response: { requestId: string; response: string; completionStatus?: 'done' | 'drill-deeper' }) {
this.mcpClient.sendResponse(response.requestId, response.response, response.completionStatus)
.subscribe({
next: () => {
console.log('Response sent successfully');
// Clear any previous error
this.recentError.set(null);
// Clear the current request
this.currentRequest.set(null);
},
error: (err) => {
console.error('Failed to send response:', err);
this.showErrorMessage('Failed to send response. Please try again.');
}
});
}
onMultipleChoiceResponseSubmitted(response: {
requestId: string;
type: 'multiple-choice';
questions: MultipleChoiceQuestion[]
}) {
this.mcpClient.sendMultipleChoiceResponse(response.requestId, response.questions)
.subscribe({
next: () => {
console.log('Multiple choice response sent successfully');
// Clear any previous error
this.recentError.set(null);
// Clear the current request
this.currentRequest.set(null);
},
error: (err) => {
console.error('Failed to send multiple choice response:', err);
this.showErrorMessage('Failed to send multiple choice response. Please try again.');
}
});
}
isMultipleChoiceRequest(request: QueuedRequest): boolean {
return (request.type === 'multiple-choice') ||
(!!request.context && typeof request.context === 'object' &&
'type' in request.context && request.context['type'] === 'multiple-choice');
}
getMultipleChoiceQuestions(request: QueuedRequest): MultipleChoiceQuestion[] {
if (!this.isMultipleChoiceRequest(request)) return [];
if (request.context && typeof request.context === 'object' && 'questions' in request.context) {
return request.context['questions'] as MultipleChoiceQuestion[];
}
return [];
}
onHypothesisChallengeResponseSubmitted(response: {
requestId: string;
type: 'hypothesis-challenge';
challenge: HypothesisChallenge;
completionStatus?: 'done' | 'drill-deeper';
}) {
this.mcpClient.sendHypothesisChallengeResponse(response.requestId, response.challenge, response.completionStatus)
.subscribe({
next: () => {
console.log('Hypothesis challenge response sent successfully');
// Clear any previous error
this.recentError.set(null);
// Clear the current request
this.currentRequest.set(null);
},
error: (err) => {
console.error('Failed to send hypothesis challenge response:', err);
this.showErrorMessage('Failed to send hypothesis challenge response. Please try again.');
}
});
}
onChooseNextResponseSubmitted(response: {
requestId: string;
type: 'choose-next';
action: 'selected' | 'abort' | 'new-ideas';
selectedOption?: any;
message?: string;
}) {
this.mcpClient.sendChooseNextResponse(response.requestId, response)
.subscribe({
next: () => {
console.log('Choose-next response sent successfully');
// Clear any previous error
this.recentError.set(null);
// Clear the current request
this.currentRequest.set(null);
},
error: (err) => {
console.error('Failed to send choose-next response:', err);
this.showErrorMessage('Failed to send choose-next response. Please try again.');
}
});
}
onToolRedirect(redirect: { requestId: string; toolName: string; message: string }) {
console.log('Tool redirect requested:', redirect);
// Send the redirect message as the response
this.mcpClient.sendResponse(redirect.requestId, redirect.message)
.subscribe({
next: () => {
console.log('Tool redirect response sent successfully');
// Clear any previous error
this.recentError.set(null);
// Clear the current request
this.currentRequest.set(null);
},
error: (err) => {
console.error('Failed to send tool redirect response:', err);
this.showErrorMessage('Failed to send tool redirect response. Please try again.');
}
});
}
isHypothesisChallengeRequest(request: QueuedRequest): boolean {
return (request.type === 'hypothesis-challenge') ||
(!!request.context && typeof request.context === 'object' &&
'type' in request.context && request.context['type'] === 'hypothesis-challenge');
}
getHypothesisChallenge(request: QueuedRequest): HypothesisChallenge {
if (!this.isHypothesisChallengeRequest(request)) {
return { id: '', title: '', hypotheses: [] };
}
if (request.context && typeof request.context === 'object' && 'challenge' in request.context) {
return request.context['challenge'] as HypothesisChallenge;
}
return { id: '', title: '', hypotheses: [] };
}
isChooseNextRequest(request: QueuedRequest): boolean {
return (request.type === 'choose-next') ||
(!!request.context && typeof request.context === 'object' &&
'type' in request.context && request.context['type'] === 'choose-next');
}
getChooseNextChallenge(request: QueuedRequest): ChooseNextChallenge {
if (!this.isChooseNextRequest(request)) {
return { id: '', title: '', description: '', options: [] };
}
if (request.context && typeof request.context === 'object' && 'challenge' in request.context) {
return request.context['challenge'] as ChooseNextChallenge;
}
return { id: '', title: '', description: '', options: [] };
}
private showTimeoutMessage(): void {
this.recentTimeout.set({
message: 'The previous question timed out after 5 minutes of no response.',
timestamp: new Date()
});
// Clear timeout message after 10 seconds
setTimeout(() => {
this.recentTimeout.set(null);
}, 10000);
}
private showCancellationMessage(): void {
this.recentCancellation.set({
message: 'The previous question was aborted by the client.',
timestamp: new Date()
});
// Clear cancellation message after 10 seconds
setTimeout(() => {
this.recentCancellation.set(null);
}, 10000);
}
private showErrorMessage(message: string): void {
this.recentError.set({
message,
timestamp: new Date()
});
// Clear error message after 10 seconds
setTimeout(() => {
this.recentError.set(null);
}, 10000);
}
}