Files
Artemis-Eapil/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html
T

325 lines
15 KiB
HTML

<!--
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="chat-interface-container">
<!-- Task Queue & History Header -->
<header class="chat-header">
<!-- Tab Selector Header inside row -->
<div class="tab-selector-bar">
<button
class="tab-selector-btn"
[class.active]="agentService.activeTab() === 'tasks'"
(click)="agentService.activeTab.set('tasks')"
>
<span class="material-symbols-outlined tab-icon">list_alt</span>
<span class="tab-text">任务队列</span>
</button>
<button
class="tab-selector-btn"
[class.active]="agentService.activeTab() === 'notes'"
(click)="agentService.activeTab.set('notes')"
>
<span class="material-symbols-outlined tab-icon">description</span>
<span class="tab-text">笔记与计划</span>
</button>
<button class="tab-selector-btn" [class.active]="agentService.activeTab() === 'automation'" (click)="agentService.activeTab.set('automation')"><span class="material-symbols-outlined tab-icon">automation</span><span class="tab-text">报告与批量</span></button>
</div>
<div class="header-right">
@if (agentService.sessions().length > 0 && agentService.activeTab() === 'tasks') {
<button
class="btn-clear-history"
(click)="clearHistory()"
title="清空所有任务与历史"
aria-label="清空所有任务与历史"
[disabled]="isSubmitting()"
>
<span class="material-symbols-outlined">delete</span>
</button>
}
</div>
</header>
<!-- Scrollable Messages/Content Area -->
<main class="chat-messages">
@if (agentService.activeTab() === 'automation') { <app-automation-panel /> }
<!-- Tasks Tab Content -->
@if (agentService.activeTab() === 'tasks') {
<app-task-batch-filter />
<!-- Active Queue Section -->
<div class="queue-section-header">
<span class="material-symbols-outlined section-icon">bolt</span> 当前队列 ({{ activeQueue().length }})
</div>
@if (activeQueue().length === 0) {
<div class="empty-section-placeholder">
没有正在执行或排队的任务。
</div>
}
@for (session of activeQueue(); track trackSession($index, session)) {
<div
class="task-card"
[class.selected]="agentService.currentSessionId() === session.session_id"
[class.running]="getTaskStatus(session) === 'running'"
(click)="selectTask(session.session_id)"
>
<div class="task-card-header">
<span class="task-badge" [ngClass]="getTaskStatus(session)">
@if (getTaskStatus(session) === 'running') {
<span class="status-dot"></span>
}
{{ getTaskStatus(session) | uiLabel }}
</span>
<app-test-result-badge [result]="session.test_result" [executionStatus]="getTaskStatus(session)" [compact]="true" />
<div class="task-card-header-actions">
@if (getDeviceSerial(session)) {
<span class="task-device" [title]="'Device: ' + getDeviceSerial(session)">
<span class="material-symbols-outlined device-icon">phone_android</span>
<span class="device-name">{{ getDeviceSerial(session) }}</span>
</span>
}
@if (getDeviceSerial(session)) {
<span class="task-time-divider">·</span>
}
<span class="task-time">{{ session.start_time * 1000 | date:'shortTime' }}</span>
@if (getTaskStatus(session) === 'running' || getTaskStatus(session) === 'paused') {
<button
type="button"
class="btn-stop-card"
(click)="stopTask(session.session_id, $event)"
title="停止此任务"
aria-label="停止此任务"
>
<span class="material-symbols-outlined">stop_circle</span>
</button>
}
@if (getTaskStatus(session) !== 'running' && getTaskStatus(session) !== 'paused') {
<button
type="button"
class="btn-delete-card"
(click)="deleteTask(session.session_id, $event)"
title="删除此任务"
aria-label="删除此任务"
[disabled]="isSubmitting()"
>
<span class="material-symbols-outlined">delete</span>
</button>
}
</div>
</div>
<div class="task-goal" title="{{ session.initial_goal }}">
{{ session.display_name || session.initial_goal || '未命名任务' }}
</div>
<div class="task-card-footer">
<app-task-id-copy [taskId]="session.session_id" />
<span class="task-date">{{ session.start_time * 1000 | date:'MM/dd/yy' }}</span>
</div>
</div>
}
<app-task-history />
}
<!-- Notes Tab Content -->
@if (agentService.activeTab() === 'notes') {
<div class="notes-tab-content">
<!-- Notes Selector / Pills (Only shown if there are multiple markdown notes) -->
@if (noteKeys().length > 1) {
<div class="notes-selector-container">
<div class="notes-selector-list">
@for (key of noteKeys(); track trackNoteKey($index, key)) {
<button
class="note-selector-pill"
[class.active]="agentService.selectedNoteKey() === key"
(click)="selectNote(key)"
>
<span class="material-symbols-outlined pill-icon">draft</span>
{{ key }}
</button>
}
</div>
</div>
}
@if (noteKeys().length === 0) {
<div class="empty-notes-container">
<div class="empty-notes-icon">
<span class="material-symbols-outlined">description</span>
</div>
<h4 class="empty-notes-title">暂无笔记与计划</h4>
<p class="empty-notes-subtitle">此会话尚未记录笔记或结构化计划。</p>
</div>
}
<!-- Notes Content Viewer (Clean card blocks matching the second screenshot style) -->
@if (noteKeys().length > 0) {
<div class="note-viewer-cards">
<!-- Render Note Title if present -->
@if (parsedNote().title) {
<h2 class="note-title">{{ parsedNote().title }}</h2>
}
<!-- Render Milestones list as Text Blocks -->
@if (parsedNote().milestones.length > 0) {
<div class="milestones-container">
@for (milestone of parsedNote().milestones; track trackMilestone($index, milestone)) {
<div
class="milestone-block"
[ngClass]="milestone.type"
>
<!-- Milestone Title Row with status icon -->
<div class="milestone-title-row">
@switch (milestone.type) {
@case ('checked') {
<span class="material-symbols-outlined milestone-status-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined milestone-status-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined milestone-status-icon unchecked">check_box_outline_blank</span>
}
}
<div class="milestone-text">
@for (seg of milestone.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
<!-- Sub-steps list if any -->
@if (milestone.subSteps.length > 0) {
<div class="milestone-substeps">
@for (step of milestone.subSteps; track trackMarkdownLine($index, step)) {
<div
class="substep-row"
[ngClass]="step.type"
[style.padding-left.px]="(step.indent > 2 ? (step.indent - 2) * 12 : 0)"
>
@switch (step.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (step.type !== 'empty') {
<span class="line-text">
@for (seg of step.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
}
</div>
}
<!-- Verification result -->
@if (milestone.checks.length > 0) {
<div class="milestone-checks-container">
@for (check of milestone.checks; track trackMarkdownLine($index, check)) {
<div
class="verification-card"
[ngClass]="check.type"
[style.margin-left.px]="(check.indent > 2 ? (check.indent - 2) * 12 : 0)"
>
<div class="card-header">
<span class="card-tag" [class.assert]="check.checkKind === 'assert'">{{ check.checkKind || 'verify' }}{{ check.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of check.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
}
</div>
}
</div>
}
</div>
}
<!-- Render any other lines (e.g. text/markdown blocks at the end) -->
@if (parsedNote().otherLines.length > 0) {
<div class="other-lines-container">
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
@if (line.type === 'verify' || line.type === 'finding') {
<div class="verification-card standalone" [ngClass]="line.type" [style.margin-left.px]="line.indent * 12">
<div class="card-header">
<span class="card-tag" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
} @else {
<div
class="md-line"
[ngClass]="line.type"
[style.margin-left.px]="line.indent * 12"
>
@switch (line.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (line.type !== 'empty') {
<span class="line-text">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
}
}
</div>
}
</div>
}
</div>
}
</main>
</div>