import { Component, Input, computed, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MarkdownModule } from 'ngx-markdown';
import { QueuedRequest, MultipleChoiceQuestion } from '@ask-me-mcp/askme-shared';
/**
* Component for displaying a human request
*
* @remarks
* This component shows the details of an MCP request including
* the question, context, and metadata. It's used in both the
* main view and the queue list.
*
* @example
* ```html
* <app-request-display [request]="activeRequest"></app-request-display>
* ```
*/
@Component({
selector: 'app-request-display',
standalone: true,
imports: [CommonModule, MarkdownModule],
template: `
<article class="request-card" [class.active]="request.status === 'active'">
<header class="request-header">
<span class="request-id">{{ request.id }}</span>
<time class="request-time" [dateTime]="request.timestamp">
{{ formatTime(request.timestamp) }}
</time>
</header>
<div class="request-body">
@if (isMarkdownQuestion()) {
<div class="request-question markdown-content">
<markdown [data]="request.question"></markdown>
</div>
} @else {
<h3 class="request-question">{{ request.question }}</h3>
}
@if (isMultipleChoice()) {
<div class="multiple-choice-preview">
<div class="question-count">{{ multipleChoiceQuestions().length }} question(s)</div>
<details class="questions-preview">
<summary>View Questions</summary>
<div class="questions-list">
@for (question of multipleChoiceQuestions(); track question.id) {
<div class="preview-question">
<strong>{{ question.text }}</strong>
<div class="preview-options">
@for (option of question.options; track option.id) {
<span class="preview-option">{{ option.text }}</span>
}
</div>
</div>
}
</div>
</details>
</div>
}
@if (request.context && hasContext && !isMultipleChoice()) {
<details class="request-context">
<summary>Context</summary>
<pre>{{ formatContext(request.context) }}</pre>
</details>
}
</div>
<footer class="request-footer">
<span class="request-status" [class]="'status-' + request.status">
{{ request.status }}
</span>
</footer>
</article>
`,
styles: [`
.request-card {
background: var(--card-bg, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
padding: 1.5rem;
transition: all 0.2s ease;
}
.request-card.active {
border-color: var(--primary-color, #007bff);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.request-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.request-id {
font-family: monospace;
font-size: 0.875rem;
color: var(--text-secondary, #666);
}
.request-time {
font-size: 0.875rem;
color: var(--text-secondary, #666);
}
.request-body {
margin-bottom: 1rem;
}
.request-question {
margin: 0 0 1rem 0;
font-size: 1.125rem;
font-weight: 500;
color: var(--text-primary, #333);
line-height: 1.5;
}
.markdown-content {
margin: 0 0 1rem 0;
}
.markdown-content ::ng-deep h1,
.markdown-content ::ng-deep h2,
.markdown-content ::ng-deep h3,
.markdown-content ::ng-deep h4,
.markdown-content ::ng-deep h5,
.markdown-content ::ng-deep h6 {
margin: 0.5rem 0;
color: var(--text-primary, #333);
}
.markdown-content ::ng-deep h1 { font-size: 1.5rem; }
.markdown-content ::ng-deep h2 { font-size: 1.375rem; }
.markdown-content ::ng-deep h3 { font-size: 1.25rem; }
.markdown-content ::ng-deep h4 { font-size: 1.125rem; }
.markdown-content ::ng-deep p {
margin: 0.5rem 0;
line-height: 1.6;
}
.markdown-content ::ng-deep ul,
.markdown-content ::ng-deep ol {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.markdown-content ::ng-deep li {
margin: 0.25rem 0;
}
.markdown-content ::ng-deep code {
background: rgba(64, 164, 223, 0.1);
padding: 0.125rem 0.25rem;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875em;
}
.markdown-content ::ng-deep pre {
background: rgba(64, 164, 223, 0.05);
border: 1px solid rgba(64, 164, 223, 0.2);
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
margin: 0.5rem 0;
}
.markdown-content ::ng-deep pre code {
background: none;
padding: 0;
}
.markdown-content ::ng-deep blockquote {
border-left: 4px solid rgba(64, 164, 223, 0.4);
background: rgba(64, 164, 223, 0.05);
margin: 0.5rem 0;
padding: 0.5rem 1rem;
font-style: italic;
}
.markdown-content ::ng-deep a {
color: #2196f3;
text-decoration: none;
}
.markdown-content ::ng-deep a:hover {
text-decoration: underline;
}
.markdown-content ::ng-deep table {
border-collapse: collapse;
width: 100%;
margin: 0.5rem 0;
}
.markdown-content ::ng-deep th,
.markdown-content ::ng-deep td {
border: 1px solid rgba(64, 164, 223, 0.2);
padding: 0.5rem;
text-align: left;
}
.markdown-content ::ng-deep th {
background: rgba(64, 164, 223, 0.1);
font-weight: 600;
}
.request-context {
margin-top: 1rem;
}
.request-context summary {
cursor: pointer;
color: var(--text-secondary, #666);
font-size: 0.875rem;
user-select: none;
}
.request-context pre {
margin: 0.5rem 0 0 0;
padding: 0.75rem;
background: var(--bg-code, #f5f5f5);
border-radius: 4px;
font-size: 0.875rem;
overflow-x: auto;
}
.request-footer {
display: flex;
justify-content: flex-end;
}
.request-status {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-pending {
background: var(--status-pending-bg, #ffc107);
color: var(--status-pending-text, #000);
}
.status-active {
background: var(--status-active-bg, #007bff);
color: var(--status-active-text, #fff);
}
.status-completed {
background: var(--status-completed-bg, #28a745);
color: var(--status-completed-text, #fff);
}
.multiple-choice-preview {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e0e0e0;
}
.question-count {
font-weight: 500;
color: #2196f3;
margin-bottom: 0.5rem;
}
.questions-preview summary {
cursor: pointer;
color: #666;
font-size: 0.875rem;
user-select: none;
}
.questions-list {
margin-top: 0.75rem;
padding-left: 1rem;
}
.preview-question {
margin-bottom: 1rem;
}
.preview-question strong {
display: block;
margin-bottom: 0.5rem;
color: #333;
}
.preview-options {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.preview-option {
background: #e3f2fd;
color: #1976d2;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
}
`]
})
export class RequestDisplayComponent {
@Input({ required: true }) request!: QueuedRequest;
requestSignal = signal<QueuedRequest | null>(null);
ngOnInit() {
this.requestSignal.set(this.request);
}
ngOnChanges() {
this.requestSignal.set(this.request);
}
isMultipleChoice = computed(() => {
const req = this.requestSignal();
return (req?.type === 'multiple-choice') ||
(!!req?.context && typeof req.context === 'object' && 'type' in req.context && req.context['type'] === 'multiple-choice');
});
multipleChoiceQuestions = computed(() => {
const req = this.requestSignal();
if (!req || !this.isMultipleChoice()) return [];
if (req.context && typeof req.context === 'object' && 'questions' in req.context) {
return req.context['questions'] as MultipleChoiceQuestion[];
}
return [];
});
get hasContext(): boolean {
return !!(this.request.context && Object.keys(this.request.context).length > 0);
}
formatTime(timestamp: Date | string): string {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
// Less than 1 minute
if (diff < 60000) {
return 'just now';
}
// Less than 1 hour
if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes}m ago`;
}
// Less than 24 hours
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
}
// Format as date
return date.toLocaleDateString();
}
formatContext(context: Record<string, unknown>): string {
return JSON.stringify(context, null, 2);
}
/**
* Detect if the question contains markdown syntax
*/
isMarkdownQuestion(): boolean {
const question = this.request.question;
if (!question) return false;
// Check for common markdown patterns
const markdownPatterns = [
/^#{1,6}\s+/m, // Headers (# ## ### etc.)
/\*\*[^*]+\*\*/, // Bold text
/\*[^*]+\*/, // Italic text (but not **bold**)
/`[^`]+`/, // Inline code
/```[\s\S]*?```/, // Code blocks
/^\* /m, // Unordered list
/^\d+\. /m, // Ordered list
/^\> /m, // Blockquote
/\[([^\]]+)\]\(([^)]+)\)/, // Links
/!\[([^\]]*)\]\(([^)]+)\)/, // Images
/\|.*\|/, // Tables
/^---+$/m, // Horizontal rule
/~~[^~]+~~/, // Strikethrough
];
// Return true if any pattern matches
return markdownPatterns.some(pattern => pattern.test(question));
}
}