325 lines
15 KiB
HTML
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>
|