Compare commits
11 Commits
agent/hex/
...
fc12ab31a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc12ab31a4 | ||
|
|
63f5790d39 | ||
|
|
4f97697d56 | ||
|
|
ab1a20a61a | ||
|
|
d473611d8c | ||
|
|
e757f9b113 | ||
|
|
bcaa526a69 | ||
|
|
14b3dab88b | ||
|
|
b97f4f0668 | ||
|
|
d5a85c4ed0 | ||
|
|
7eaf736fc4 |
@@ -8,12 +8,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.14" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<article
|
||||||
|
class="agent-card"
|
||||||
|
[class]="'agent-card--' + status()"
|
||||||
|
role="article"
|
||||||
|
[attr.aria-label]="'Agent card: ' + statusLabel() + ' status'"
|
||||||
|
[style.--agent-status-color]="statusColorVar()"
|
||||||
|
[style.--agent-status-bg]="statusBgVar()"
|
||||||
|
>
|
||||||
|
<!-- Left border accent (4px, matching status color) -->
|
||||||
|
<div class="agent-card__accent"></div>
|
||||||
|
|
||||||
|
<!-- Card body -->
|
||||||
|
<div class="agent-card__body">
|
||||||
|
<!-- Header row: status badge + channel + last activity -->
|
||||||
|
<div class="agent-card__header">
|
||||||
|
<div class="agent-card__status-row">
|
||||||
|
<!-- Agent Status Badge (CUB-48) -->
|
||||||
|
<app-agent-status-badge
|
||||||
|
[status]="status()"
|
||||||
|
[showLabel]="true"
|
||||||
|
></app-agent-status-badge>
|
||||||
|
</div>
|
||||||
|
<div class="agent-card__meta">
|
||||||
|
@if (channel()) {
|
||||||
|
<span class="agent-card__channel text-mono">
|
||||||
|
<mat-icon class="agent-card__channel-icon" fontIcon="forum" [inline]="true"></mat-icon>
|
||||||
|
{{ channel() }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<span class="agent-card__last-activity text-mono">{{ lastActivityText() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task description -->
|
||||||
|
@if (task()) {
|
||||||
|
<p class="agent-card__task">{{ task() }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Task Progress Bar (CUB-44) -->
|
||||||
|
@if (showProgress()) {
|
||||||
|
<app-task-progress-bar
|
||||||
|
[progress]="progress()"
|
||||||
|
[showElapsed]="false"
|
||||||
|
></app-task-progress-bar>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Footer: session key (truncated) + Quick-Jump Button (CUB-46) -->
|
||||||
|
<div class="agent-card__footer">
|
||||||
|
<span class="agent-card__session text-mono" [matTooltip]="sessionKey()">
|
||||||
|
{{ sessionKey().length > 28 ? sessionKey().substring(0, 28) + '…' : sessionKey() }}
|
||||||
|
</span>
|
||||||
|
<app-quick-jump-button
|
||||||
|
[sessionKey]="sessionKey()"
|
||||||
|
(jumpClick)="jumpClick.emit($event)"
|
||||||
|
></app-quick-jump-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
192
frontend/src/app/components/agent-card/agent-card.component.scss
Normal file
192
frontend/src/app/components/agent-card/agent-card.component.scss
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Agent Card Styles — M3 Tactical Dark (Final Integration, CUB-45)
|
||||||
|
// Uses sub-components:
|
||||||
|
// - AgentStatusBadge (CUB-48) for status display
|
||||||
|
// - TaskProgressBar (CUB-44) for progress
|
||||||
|
// - QuickJumpButton (CUB-46) for navigation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Card Shell
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
min-width: var(--cc-card-min-width, 320px);
|
||||||
|
border-radius: var(--cc-card-border-radius, 16px);
|
||||||
|
background-color: var(--cc-surface-container);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: background-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
|
||||||
|
// M3 state layer overlay on hover (8% primary)
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus-visible outline for keyboard navigation
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 3px solid var(--status-active);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Left-border accent (4px, matching status color)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card__accent {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 4px;
|
||||||
|
background-color: var(--agent-status-color);
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Card Body
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card__body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: var(--cc-card-padding, 20px);
|
||||||
|
min-width: 0; // Prevent flex blowout for long text
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Header Row
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--cc-on-surface-variant);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__channel {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__channel-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__last-activity {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Task Description
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card__task {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--cc-on-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Task Progress Bar (CUB-44 sub-component)
|
||||||
|
// Override the sub-component's progress bar colors to match status color
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
:host ::ng-deep .task-progress-bar .mat-mdc-progress-bar,
|
||||||
|
.agent-card .mat-mdc-progress-bar {
|
||||||
|
--mdc-linear-progress-active-indicator-color: var(--agent-status-color);
|
||||||
|
--mdc-linear-progress-track-color: var(--agent-status-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Footer Row
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto; // Push footer to bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__session {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--cc-on-surface-variant);
|
||||||
|
opacity: 0.6;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status-specific card backgrounds (subtle tint)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.agent-card--active {
|
||||||
|
background-color: var(--status-active-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card--thinking {
|
||||||
|
background-color: var(--status-thinking-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card--error {
|
||||||
|
background-color: var(--status-error-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle and offline use default surface-container
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Accessibility: Reduced Motion
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.agent-card {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Responsive: Mobile adjustments
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
.agent-card {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card__body {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
frontend/src/app/components/agent-card/agent-card.component.ts
Normal file
151
frontend/src/app/components/agent-card/agent-card.component.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Agent Card Component — Final Integration
|
||||||
|
// Per CUB-45: Compose AgentCard from all sub-components.
|
||||||
|
// - AgentStatusBadge (CUB-48) for status display
|
||||||
|
// - TaskProgressBar (CUB-44) for progress indication
|
||||||
|
// - QuickJumpButton (CUB-46) for session navigation
|
||||||
|
// Layout: left-border accent, aria-labels, role="article"
|
||||||
|
// ============================================================================
|
||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Output, input, computed } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
// Sub-components (CUB-48, CUB-44, CUB-46)
|
||||||
|
import { AgentStatusBadgeComponent } from '../agent-status-badge';
|
||||||
|
import { TaskProgressBarComponent } from '../task-progress-bar';
|
||||||
|
import { QuickJumpButtonComponent } from '../quick-jump-button';
|
||||||
|
|
||||||
|
import { AgentStatus } from '../../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentCard displays a single agent's status in the Command Hub grid.
|
||||||
|
*
|
||||||
|
* Composes three sub-components:
|
||||||
|
* - AgentStatusBadge: colored pill with pulse animation (CUB-48)
|
||||||
|
* - TaskProgressBar: determinate progress with optional elapsed time (CUB-44)
|
||||||
|
* - QuickJumpButton: M3 FilledTonal icon button for session navigation (CUB-46)
|
||||||
|
*
|
||||||
|
* Inputs:
|
||||||
|
* - status: AgentStatus — current agent status
|
||||||
|
* - task: string — current task description
|
||||||
|
* - progress: number — task progress percentage (0–100)
|
||||||
|
* - sessionKey: string — full session key
|
||||||
|
* - channel: string — communication channel (e.g., "telegram")
|
||||||
|
* - lastActivity: Date — timestamp of last activity
|
||||||
|
*
|
||||||
|
* Outputs:
|
||||||
|
* - jumpClick: string — emitted when Quick-Jump button is clicked, carries sessionKey
|
||||||
|
*
|
||||||
|
* Accessibility:
|
||||||
|
* - role="article" on the card element
|
||||||
|
* - aria-label on the card summarizing status
|
||||||
|
* - aria-label on the progress bar (via TaskProgressBar)
|
||||||
|
* - aria-label on the Quick-Jump button (via QuickJumpButton)
|
||||||
|
* - focus-visible outlines for keyboard navigation
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-agent-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
// Sub-components
|
||||||
|
AgentStatusBadgeComponent,
|
||||||
|
TaskProgressBarComponent,
|
||||||
|
QuickJumpButtonComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './agent-card.component.html',
|
||||||
|
styleUrl: './agent-card.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AgentCardComponent {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Inputs (all 6 required by CUB-45 definition of done)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Current agent status */
|
||||||
|
readonly status = input.required<AgentStatus>();
|
||||||
|
|
||||||
|
/** Current task description */
|
||||||
|
readonly task = input<string>('');
|
||||||
|
|
||||||
|
/** Task progress percentage (0–100) */
|
||||||
|
readonly progress = input<number>(0);
|
||||||
|
|
||||||
|
/** Full session key */
|
||||||
|
readonly sessionKey = input.required<string>();
|
||||||
|
|
||||||
|
/** Communication channel (e.g., "telegram") */
|
||||||
|
readonly channel = input<string>('');
|
||||||
|
|
||||||
|
/** Timestamp of last activity */
|
||||||
|
readonly lastActivity = input<Date>(new Date());
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Outputs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Emitted when the Quick-Jump button is clicked with the session key. */
|
||||||
|
@Output() readonly jumpClick = new EventEmitter<string>();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Computed values
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Map status to CSS custom property name for dynamic color binding */
|
||||||
|
readonly statusColorVar = computed(() => {
|
||||||
|
const map: Record<AgentStatus, string> = {
|
||||||
|
active: 'var(--status-active)',
|
||||||
|
idle: 'var(--status-idle)',
|
||||||
|
thinking: 'var(--status-thinking)',
|
||||||
|
error: 'var(--status-error)',
|
||||||
|
offline: 'var(--status-offline)',
|
||||||
|
};
|
||||||
|
return map[this.status()];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Map status to background tint CSS variable */
|
||||||
|
readonly statusBgVar = computed(() => {
|
||||||
|
const map: Record<AgentStatus, string> = {
|
||||||
|
active: 'var(--status-active-bg)',
|
||||||
|
idle: 'var(--status-idle-bg)',
|
||||||
|
thinking: 'var(--status-thinking-bg)',
|
||||||
|
error: 'var(--status-error-bg)',
|
||||||
|
offline: 'var(--cc-surface-container)',
|
||||||
|
};
|
||||||
|
return map[this.status()];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Human-readable status label (delegates to AgentStatusBadgeComponent) */
|
||||||
|
readonly statusLabel = computed(() => {
|
||||||
|
const map: Record<AgentStatus, string> = {
|
||||||
|
active: 'Active',
|
||||||
|
idle: 'Idle',
|
||||||
|
thinking: 'Thinking',
|
||||||
|
error: 'Error',
|
||||||
|
offline: 'Offline',
|
||||||
|
};
|
||||||
|
return map[this.status()];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Format last activity as relative time string */
|
||||||
|
readonly lastActivityText = computed(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const then = this.lastActivity().getTime();
|
||||||
|
const diffMs = now - then;
|
||||||
|
const diffMin = Math.floor(diffMs / 60000);
|
||||||
|
if (diffMin < 1) return 'Just now';
|
||||||
|
if (diffMin < 60) return `${diffMin}m ago`;
|
||||||
|
const diffHr = Math.floor(diffMin / 60);
|
||||||
|
if (diffHr < 24) return `${diffHr}h ago`;
|
||||||
|
return `${Math.floor(diffHr / 24)}d ago`;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Whether to show progress bar */
|
||||||
|
readonly showProgress = computed(() => {
|
||||||
|
const s = this.status();
|
||||||
|
return (s === 'active' || s === 'thinking') && this.progress() > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
6
frontend/src/app/components/agent-card/index.ts
Normal file
6
frontend/src/app/components/agent-card/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Agent Card — Barrel Export
|
||||||
|
// CUB-45: Final integration with sub-components
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export { AgentCardComponent } from './agent-card.component';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!-- Agent Status Badge: colored pill with pulse animation -->
|
||||||
|
<span
|
||||||
|
class="status-badge"
|
||||||
|
[class]="statusClass"
|
||||||
|
[attr.aria-label]="statusLabels[status] + ' status'"
|
||||||
|
[attr.role]="'status'"
|
||||||
|
>
|
||||||
|
<span class="status-badge__dot"></span>
|
||||||
|
<span *ngIf="showLabel" class="status-badge__label">
|
||||||
|
{{ statusLabels[status] }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Agent Status Badge — Pill Style with Pulse Animation
|
||||||
|
// Per CUB-48: Color mapping + animation durations:
|
||||||
|
// Active → --color-primary (#38BDF8) 2s pulse
|
||||||
|
// Idle → --color-secondary (#2DD4BF) no animation
|
||||||
|
// Thinking → --color-accent (#A78BFA) 3s pulse
|
||||||
|
// Error → --color-danger (#F87171) 0.8s pulse
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Badge Container
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
// --- Status-specific backgrounds & text ---
|
||||||
|
&--active {
|
||||||
|
background-color: var(--status-active-bg);
|
||||||
|
color: var(--status-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--idle {
|
||||||
|
background-color: var(--status-idle-bg);
|
||||||
|
color: var(--status-idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--thinking {
|
||||||
|
background-color: var(--status-thinking-bg);
|
||||||
|
color: var(--status-thinking);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background-color: var(--status-error-bg);
|
||||||
|
color: var(--status-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--offline {
|
||||||
|
background-color: rgba(100, 116, 139, 0.12);
|
||||||
|
color: var(--status-offline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status Dot (inner indicator circle)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.status-badge__dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.status-badge--active & {
|
||||||
|
background-color: var(--status-active);
|
||||||
|
animation: badge-pulse-active 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--idle & {
|
||||||
|
background-color: var(--status-idle);
|
||||||
|
// Idle: no animation — steady dot
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--thinking & {
|
||||||
|
background-color: var(--status-thinking);
|
||||||
|
animation: badge-pulse-thinking 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--error & {
|
||||||
|
background-color: var(--status-error);
|
||||||
|
animation: badge-pulse-error 0.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--offline & {
|
||||||
|
background-color: var(--status-offline);
|
||||||
|
// Offline: no animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Label Text
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.status-badge__label {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Badge Pulse Keyframes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// These are scoped to the badge component rather than reusing the global
|
||||||
|
// .status-dot animations, because the badge pulse is subtler (scale + opacity
|
||||||
|
// blend for a pill context vs. standalone dot context).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@keyframes badge-pulse-active {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-pulse-thinking {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-pulse-error {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Accessibility: Reduced Motion
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.status-badge__dot {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Agent Status Badge Component
|
||||||
|
// Per CUB-48: Colored pill badge with pulse animation for agent statuses.
|
||||||
|
// Displays Active, Idle, Thinking, or Error with correct color mapping.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AgentStatus } from '../../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable status badge that renders a colored pill with label text
|
||||||
|
* and a subtle pulse animation for Active, Thinking, and Error states.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```html
|
||||||
|
* <app-agent-status-badge status="active" />
|
||||||
|
* <app-agent-status-badge status="error" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-agent-status-badge',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './agent-status-badge.component.html',
|
||||||
|
styleUrl: './agent-status-badge.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AgentStatusBadgeComponent {
|
||||||
|
/** Agent status to display. Maps to a color and animation. */
|
||||||
|
@Input({ required: true })
|
||||||
|
status!: AgentStatus;
|
||||||
|
|
||||||
|
/** Whether to show the status label text alongside the dot. Defaults to true. */
|
||||||
|
@Input()
|
||||||
|
showLabel = true;
|
||||||
|
|
||||||
|
/** Mapping from status to human-readable label. */
|
||||||
|
readonly statusLabels: Record<AgentStatus, string> = {
|
||||||
|
active: 'Active',
|
||||||
|
idle: 'Idle',
|
||||||
|
thinking: 'Thinking',
|
||||||
|
error: 'Error',
|
||||||
|
offline: 'Offline',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** CSS class string for the badge container based on current status. */
|
||||||
|
get statusClass(): string {
|
||||||
|
return `status-badge--${this.status}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/src/app/components/agent-status-badge/index.ts
Normal file
6
frontend/src/app/components/agent-status-badge/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Agent Status Badge — Barrel Export
|
||||||
|
// CUB-48
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export { AgentStatusBadgeComponent } from './agent-status-badge.component';
|
||||||
8
frontend/src/app/components/index.ts
Normal file
8
frontend/src/app/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Components Barrel Export
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export * from './agent-card';
|
||||||
|
export * from './agent-status-badge';
|
||||||
|
export * from './task-progress-bar';
|
||||||
|
export * from './quick-jump-button';
|
||||||
6
frontend/src/app/components/quick-jump-button/index.ts
Normal file
6
frontend/src/app/components/quick-jump-button/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Quick-Jump Button — Barrel Export
|
||||||
|
// CUB-46
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export { QuickJumpButtonComponent } from './quick-jump-button.component';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
class="quick-jump-button"
|
||||||
|
[attr.aria-label]="'Jump to agent session'"
|
||||||
|
(click)="onJumpClick()"
|
||||||
|
>
|
||||||
|
<mat-icon>arrow_forward</mat-icon>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Quick-Jump Button — M3 FilledTonalIconButton
|
||||||
|
// Per spec Section 7.3: Agent Card Quick-Jump action
|
||||||
|
// M3 spec: FilledTonalIconButton uses secondary container color
|
||||||
|
// with 8% state layer overlay for hover/focus.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
.quick-jump-button {
|
||||||
|
// M3 FilledTonalIconButton: secondary-container background
|
||||||
|
// Angular Material mat-icon-button sets up the base shape (40x40, round).
|
||||||
|
// We override the color tokens to match FilledTonal style.
|
||||||
|
--mdc-icon-button-icon-color: var(--mat-sys-on-secondary-container);
|
||||||
|
background-color: var(--mat-sys-secondary-container);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
// M3 State Layer: 8% overlay on hover
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--mat-sys-secondary-container);
|
||||||
|
// State layer overlay using a pseudo-element for precise 8% opacity
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--mat-sys-on-secondary-container);
|
||||||
|
opacity: 0.08;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// M3 State Layer: 12% overlay on focus-visible (slightly stronger for accessibility)
|
||||||
|
&:focus-visible {
|
||||||
|
background-color: var(--mat-sys-secondary-container);
|
||||||
|
outline: 3px solid var(--status-active);
|
||||||
|
outline-offset: 2px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--mat-sys-on-secondary-container);
|
||||||
|
opacity: 0.12;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// M3 State Layer: 12% overlay on active/pressed
|
||||||
|
&:active {
|
||||||
|
background-color: var(--mat-sys-secondary-container);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--mat-sys-on-secondary-container);
|
||||||
|
opacity: 0.12;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon color stays on-secondary-container across all states
|
||||||
|
.mat-icon {
|
||||||
|
color: var(--mat-sys-on-secondary-container);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { MatIconButton } from '@angular/material/button';
|
||||||
|
import { MatIcon } from '@angular/material/icon';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick-Jump Button — M3 FilledTonalIconButton
|
||||||
|
*
|
||||||
|
* An icon button that emits a navigation event for jumping to an agent session.
|
||||||
|
* Uses the Material Design 3 FilledTonalIconButton style with 8% state layer
|
||||||
|
* overlay on hover and focus.
|
||||||
|
*
|
||||||
|
* Per spec Section 7.3: Agent Card Component Interface
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-quick-jump-button',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatIconButton, MatIcon],
|
||||||
|
templateUrl: './quick-jump-button.component.html',
|
||||||
|
styleUrl: './quick-jump-button.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class QuickJumpButtonComponent {
|
||||||
|
/** The session key to navigate to. Set by the parent agent card. */
|
||||||
|
@Input()
|
||||||
|
sessionKey = '';
|
||||||
|
|
||||||
|
/** Emitted when the button is clicked, carrying the session key for navigation. */
|
||||||
|
@Output() jumpClick = new EventEmitter<string>();
|
||||||
|
|
||||||
|
onJumpClick(): void {
|
||||||
|
this.jumpClick.emit(this.sessionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/src/app/components/task-progress-bar/index.ts
Normal file
6
frontend/src/app/components/task-progress-bar/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Task Progress Bar — Barrel Export
|
||||||
|
// CUB-44
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export { TaskProgressBarComponent } from './task-progress-bar.component';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- Task Progress Bar: determinate progress with optional elapsed time -->
|
||||||
|
<div class="task-progress-bar">
|
||||||
|
<!-- Info row: percentage + optional elapsed -->
|
||||||
|
<div class="task-progress-bar__info">
|
||||||
|
<span class="task-progress-bar__percent">{{ clampedProgress }}%</span>
|
||||||
|
<span *ngIf="showElapsed" class="task-progress-bar__elapsed">
|
||||||
|
{{ elapsedText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Angular Material determinate progress bar -->
|
||||||
|
<mat-progress-bar
|
||||||
|
class="task-progress-bar__bar"
|
||||||
|
mode="determinate"
|
||||||
|
[value]="clampedProgress"
|
||||||
|
aria-label="Task progress"
|
||||||
|
></mat-progress-bar>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Task Progress Bar — Tactical Dark Theme Styling
|
||||||
|
// Per CUB-44: Uses --color-primary for bar fill and --color-surface-light
|
||||||
|
// for track background, mapped to the Control Center's M3 dark tokens.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Container
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.task-progress-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Info row: percentage label + elapsed time
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
.task-progress-bar__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-progress-bar__percent {
|
||||||
|
font-family: var(--cc-font-mono, 'Roboto Mono', monospace);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--cc-on-surface, #E2E8F0);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-progress-bar__elapsed {
|
||||||
|
font-family: var(--cc-font-mono, 'Roboto Mono', monospace);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--cc-on-surface-variant, #8A9BB0);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Material Progress Bar Overrides
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Map the spec's --color-primary and --color-surface-light to the Control
|
||||||
|
// Center's actual theme tokens. This ensures the bar uses the tactical dark
|
||||||
|
// palette while respecting the spec's variable naming.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.task-progress-bar__bar {
|
||||||
|
// Override the track (background) to use the surface container
|
||||||
|
--mat-progress-bar-track-height: 6px;
|
||||||
|
--mat-progress-bar-active-indicator-height: 6px;
|
||||||
|
|
||||||
|
// Bar fill color: primary (cyan/sky blue per tactical dark theme)
|
||||||
|
--mat-progress-bar-active-indicator-color: var(--color-primary, var(--mat-sys-primary, #38BDF8));
|
||||||
|
|
||||||
|
// Track background: surface container (dark slate)
|
||||||
|
--mat-progress-bar-track-color: var(--color-surface-light, var(--cc-surface-container, #1C2027));
|
||||||
|
|
||||||
|
// Border radius for a softer bar
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
// Smooth transition on value changes
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounded ends on the progress bar fill
|
||||||
|
:host ::ng-deep .mdc-linear-progress__bar-inner {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounded track background
|
||||||
|
:host ::ng-deep .mdc-linear-progress__track {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Task Progress Bar Component
|
||||||
|
// Per CUB-44: Determinate progress bar with optional elapsed time display.
|
||||||
|
// Uses Angular Material mat-progress-bar in determinate mode with tactical
|
||||||
|
// dark theme styling via CSS custom properties.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a determinate progress bar with an optional elapsed time indicator.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```html
|
||||||
|
* <app-task-progress-bar [progress]="65" />
|
||||||
|
* <app-task-progress-bar [progress]="42" [showElapsed]="true" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-task-progress-bar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatProgressBarModule],
|
||||||
|
templateUrl: './task-progress-bar.component.html',
|
||||||
|
styleUrl: './task-progress-bar.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class TaskProgressBarComponent implements OnInit, OnDestroy {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Inputs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Current progress percentage (0–100). Required. */
|
||||||
|
@Input({ required: true })
|
||||||
|
progress!: number;
|
||||||
|
|
||||||
|
/** Whether to show elapsed time next to the percentage. Defaults to false. */
|
||||||
|
@Input()
|
||||||
|
showElapsed = false;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Timestamp when the component initialized — used for elapsed calculation. */
|
||||||
|
startTime = Date.now();
|
||||||
|
|
||||||
|
/** Formatted elapsed time string, e.g. "2m 15s ago". */
|
||||||
|
elapsedText = '';
|
||||||
|
|
||||||
|
/** Interval timer for updating the elapsed display. */
|
||||||
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lifecycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updateElapsed();
|
||||||
|
|
||||||
|
if (this.showElapsed) {
|
||||||
|
// Update elapsed time every second
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.updateElapsed();
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Clamp progress to 0–100 for safety. */
|
||||||
|
get clampedProgress(): number {
|
||||||
|
return Math.max(0, Math.min(100, this.progress ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recalculate the elapsed time string. */
|
||||||
|
private updateElapsed(): void {
|
||||||
|
const elapsedMs = Date.now() - this.startTime;
|
||||||
|
const totalSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
this.elapsedText = `${minutes}m ${seconds}s ago`;
|
||||||
|
} else {
|
||||||
|
this.elapsedText = `${seconds}s ago`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,88 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||||
|
import { AgentCardComponent } from '../../components/agent-card/agent-card.component';
|
||||||
|
import { AgentCardData } from '../../models/agent.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hub-page',
|
selector: 'app-hub-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [],
|
imports: [AgentCardComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="hub-page">
|
<div class="hub-page">
|
||||||
<p class="hub-page__placeholder">Command Hub — Fleet status grid will render here</p>
|
<div class="hub-page__grid">
|
||||||
|
@for (agent of agents(); track agent.id) {
|
||||||
|
<app-agent-card
|
||||||
|
[status]="agent.status"
|
||||||
|
[task]="agent.currentTask ?? ''"
|
||||||
|
[progress]="agent.taskProgress ?? 0"
|
||||||
|
[sessionKey]="agent.sessionKey"
|
||||||
|
[channel]="agent.channel"
|
||||||
|
[lastActivity]="agent.lastActivity"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
.hub-page {
|
.hub-page {
|
||||||
display: flex;
|
padding: var(--cc-section-padding, 24px);
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
}
|
||||||
.hub-page__placeholder {
|
|
||||||
color: var(--cc-on-surface-variant);
|
.hub-page__grid {
|
||||||
font-size: 16px;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(var(--cc-card-min-width, 320px), 1fr));
|
||||||
|
gap: var(--cc-card-gap, 16px);
|
||||||
}
|
}
|
||||||
`],
|
`],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class HubPageComponent {}
|
export class HubPageComponent {
|
||||||
|
/** Demo agents for development — will be replaced by SignalR data */
|
||||||
|
protected readonly agents = signal<AgentCardData[]>([
|
||||||
|
{
|
||||||
|
id: 'otto',
|
||||||
|
displayName: 'Otto',
|
||||||
|
role: 'Orchestrator',
|
||||||
|
status: 'active',
|
||||||
|
currentTask: 'Reviewing PR #42',
|
||||||
|
taskProgress: 65,
|
||||||
|
taskElapsed: '04m 12s',
|
||||||
|
sessionKey: 'agent:otto:telegram:direct:8787451565',
|
||||||
|
channel: 'telegram',
|
||||||
|
lastActivity: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rex',
|
||||||
|
displayName: 'Rex',
|
||||||
|
role: 'Frontend Specialist',
|
||||||
|
status: 'thinking',
|
||||||
|
currentTask: 'Building AgentCard component',
|
||||||
|
taskProgress: 30,
|
||||||
|
taskElapsed: '02m 45s',
|
||||||
|
sessionKey: 'agent:rex:subagent:0cdbf600',
|
||||||
|
channel: 'telegram',
|
||||||
|
lastActivity: new Date(Date.now() - 120000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dex',
|
||||||
|
displayName: 'Dex',
|
||||||
|
role: 'Backend Engineer',
|
||||||
|
status: 'idle',
|
||||||
|
sessionKey: 'agent:dex:slack:channel:C01234567',
|
||||||
|
channel: 'slack',
|
||||||
|
lastActivity: new Date(Date.now() - 300000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hex',
|
||||||
|
displayName: 'Hex',
|
||||||
|
role: 'Database Architect',
|
||||||
|
status: 'error',
|
||||||
|
currentTask: 'Migration failed — rollback',
|
||||||
|
taskProgress: 0,
|
||||||
|
taskElapsed: '00m 00s',
|
||||||
|
sessionKey: 'agent:hex:slack:channel:C01234568',
|
||||||
|
channel: 'slack',
|
||||||
|
lastActivity: new Date(Date.now() - 1800000),
|
||||||
|
errorMessage: 'Connection timeout',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -48,6 +48,17 @@ html {
|
|||||||
// These are NOT part of the M3 tonal palette; they are semantic overrides.
|
// These are NOT part of the M3 tonal palette; they are semantic overrides.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
:root {
|
:root {
|
||||||
|
// --- Tactical Dark Mode color palette (CUB-47) ---
|
||||||
|
--color-surface: #0F172A;
|
||||||
|
--color-surface-light: #1E293B;
|
||||||
|
--color-primary: #38BDF8;
|
||||||
|
--color-secondary: #2DD4BF;
|
||||||
|
--color-accent: #A78BFA;
|
||||||
|
--color-danger: #F87171;
|
||||||
|
--color-text-primary: #FFFFFF;
|
||||||
|
--color-text-secondary: #94A3B8;
|
||||||
|
--color-border: #334155;
|
||||||
|
|
||||||
// --- Status colors ---
|
// --- Status colors ---
|
||||||
--status-active: #38BDF8;
|
--status-active: #38BDF8;
|
||||||
--status-idle: #2DD4BF;
|
--status-idle: #2DD4BF;
|
||||||
@@ -90,7 +101,7 @@ html {
|
|||||||
// Global Body Styles
|
// Global Body Styles
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
body {
|
body {
|
||||||
background-color: var(--cc-background);
|
background-color: var(--color-surface);
|
||||||
color: var(--cc-on-surface);
|
color: var(--cc-on-surface);
|
||||||
font-family: 'Inter', 'Roboto', sans-serif;
|
font-family: 'Inter', 'Roboto', sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user