feat: prepare 2.2.0 with UI and execution fixes
Add dark theme, richer Markdown and readable plugin results. Improve Android input evidence, response integrity and diagnostics. Unify lifecycle, guidance receipts and evidence sources across clients. Keep direct action dispatch without an added AI quota approval layer. Align version metadata at 2.2.0 and bundle Helper 1.2.2. Validation: 3063 backend tests before the version bump; 44 after. Frontend: 317 tests and production build passed.
This commit is contained in:
1 parent
83bcfd54d5
commit
fac4ccb760
146 files changed
+5689
-2033
No files matched your search
@@ -0,0 +1,41 @@
|
||||
// Shared markup appears in component innerHTML and in body-mounted overlays.
|
||||
:is(.report-prose, .stream-text, .markdown-content) {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
a { color: var(--app-primary-text, #2563eb); text-decoration: underline; }
|
||||
img { max-width: 100%; height: auto; border-radius: 8px; }
|
||||
.md-table-scroll { max-width: 100%; overflow-x: auto; margin: 10px 0; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th, td {
|
||||
border: 1px solid var(--app-border-control, #cbd5e1);
|
||||
padding: 7px 9px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th { background: var(--app-primary-soft, #eff6ff); font-weight: 600; }
|
||||
.md-table-scroll .md-align-left { text-align: left; }
|
||||
.md-table-scroll .md-align-center { text-align: center; }
|
||||
.md-table-scroll .md-align-right { text-align: right; }
|
||||
pre { max-width: 100%; overflow-x: auto; white-space: pre; }
|
||||
ul, ol { padding-left: 24px; }
|
||||
li > ul, li > ol { margin: 4px 0; }
|
||||
.md-task-item { list-style: none; }
|
||||
.md-task-marker { display: inline-block; margin-left: -18px; margin-right: 4px; }
|
||||
.md-task-marker.checked { color: var(--app-success-text, #047857); }
|
||||
.md-task-marker.progress { color: var(--app-primary-text, #2563eb); }
|
||||
.verify-badge, .finding-badge {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: var(--app-primary-soft, #eff6ff);
|
||||
color: var(--app-primary-text, #1d4ed8);
|
||||
}
|
||||
.verify-badge.assert, .finding-badge {
|
||||
background: var(--app-warning-soft, #fffbeb);
|
||||
color: var(--app-warning-text, #92400e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@use 'sass:color';
|
||||
|
||||
// Light values stay beside each existing declaration as fallbacks. These shared
|
||||
// semantic overrides also reach CDK overlays mounted outside routed components.
|
||||
:root[data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
@each $role, $value in (
|
||||
canvas: #0b1220,
|
||||
surface: #111c2e,
|
||||
surface-soft: #172337,
|
||||
surface-raised: #1e2c42,
|
||||
text-strong: #f1f5f9,
|
||||
text-control: #dbe4f0,
|
||||
text-secondary: #bdcadd,
|
||||
text-muted: #98aac1,
|
||||
text-faint: #8da1bb,
|
||||
border: #2b3b52,
|
||||
border-control: #3d506a,
|
||||
highlight: #24344d,
|
||||
primary-text: #8ebcff,
|
||||
primary-soft: #152e50,
|
||||
primary-border: #375b83,
|
||||
exploration-text: #b7b3ff,
|
||||
exploration-soft: #29264b,
|
||||
exploration-border: #514a83,
|
||||
success-text: #6ee7b7,
|
||||
success-soft: #103b32,
|
||||
success-border: #24634d,
|
||||
warning-text: #fcd089,
|
||||
warning-soft: #3b2b16,
|
||||
warning-border: #775728,
|
||||
error-text: #ff9ca7,
|
||||
error-soft: #421f2b,
|
||||
error-border: #7d3d4e,
|
||||
info-text: #83d8f5,
|
||||
info-soft: #153747,
|
||||
info-border: #316479
|
||||
) {
|
||||
--app-#{$role}: #{$value};
|
||||
--app-#{$role}-rgb: #{color.channel($value, 'red', $space: rgb)} #{color.channel($value, 'green', $space: rgb)} #{color.channel($value, 'blue', $space: rgb)};
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
background: var(--app-canvas, #f8f9fa);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--app-canvas, #f8f9fa);
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--app-primary-border, #bfdbfe);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ limitations under the License.
|
||||
</button>
|
||||
<button #automationOrigin="cdkOverlayOrigin" cdkOverlayOrigin class="vscode-tab-btn" [class.active]="isAutomationOpen()" (click)="isAutomationOpen.set(!isAutomationOpen())" title="报告与批量" aria-label="报告与批量"><span class="material-symbols-outlined tab-icon">assignment</span><span>报告与批量</span></button>
|
||||
</div>
|
||||
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="automationOrigin" [cdkConnectedOverlayOpen]="isAutomationOpen()" [cdkConnectedOverlayHasBackdrop]="true" cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop" (backdropClick)="isAutomationOpen.set(false)" [cdkConnectedOverlayPositions]="dropdownPositions"><div class="vscode-dropdown-menu" style="width:min(560px,calc(100vw - 24px));max-height:85vh;overflow:auto;padding:20px"><div class="popover-title-header" style="justify-content:space-between;margin-bottom:6px"><span>报告与自动化</span><button type="button" (click)="isAutomationOpen.set(false)" aria-label="关闭报告与自动化" style="border:0;background:transparent;color:#64748b;cursor:pointer;padding:4px"><span class="material-symbols-outlined">close</span></button></div><app-automation-panel (queueRequested)="openBatchQueue()" /></div></ng-template>
|
||||
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="automationOrigin" [cdkConnectedOverlayOpen]="isAutomationOpen()" [cdkConnectedOverlayHasBackdrop]="true" cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop" (backdropClick)="isAutomationOpen.set(false)" [cdkConnectedOverlayPositions]="dropdownPositions"><div class="vscode-dropdown-menu" style="width:min(560px,calc(100vw - 24px));max-height:85vh;overflow:auto;padding:20px"><div class="popover-title-header" style="justify-content:space-between;margin-bottom:6px"><span>报告与自动化</span><button type="button" (click)="isAutomationOpen.set(false)" aria-label="关闭报告与自动化" style="border:0;background:transparent;color:var(--app-text-muted, #64748b);cursor:pointer;padding:4px"><span class="material-symbols-outlined">close</span></button></div><app-automation-panel (queueRequested)="openBatchQueue()" /></div></ng-template>
|
||||
|
||||
<!-- Dropdown Popover Panel for Task Queue -->
|
||||
<ng-template
|
||||
@@ -87,7 +87,7 @@ limitations under the License.
|
||||
@if (getTaskStatus(session) === 'running') {
|
||||
<span class="status-dot"></span>
|
||||
}
|
||||
{{ getTaskStatus(session) | uiLabel }}
|
||||
{{ taskPhaseLabel(session) || (getTaskStatus(session) | uiLabel) }}
|
||||
</span>
|
||||
<app-test-result-badge [result]="session.test_result" [executionStatus]="getTaskStatus(session)" [compact]="true" />
|
||||
</div>
|
||||
@@ -183,160 +183,7 @@ limitations under the License.
|
||||
<!-- Rich Notes Content Viewer -->
|
||||
@if (noteKeys().length > 0) {
|
||||
<div class="dropdown-notes-content">
|
||||
@if (agentService.selectedNoteKey() !== 'task_plan.md') {
|
||||
<app-report-markdown [content]="currentNoteContent()" />
|
||||
} @else {
|
||||
@if (parsedNote().title) {
|
||||
<h3 class="note-title">{{ parsedNote().title }}</h3>
|
||||
}
|
||||
<!-- Milestones list -->
|
||||
@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"
|
||||
>
|
||||
<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 (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>
|
||||
}
|
||||
<!-- Other Markdown Lines Container -->
|
||||
@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>
|
||||
}
|
||||
}
|
||||
<app-report-markdown [content]="currentNoteContent()" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -514,6 +361,7 @@ limitations under the License.
|
||||
<!-- Event Logs Section -->
|
||||
<main class="stream-logs-content">
|
||||
<ng-content select="[task-summary]"></ng-content>
|
||||
<app-task-lifecycle [value]="agentService.currentLifecycle()" />
|
||||
<app-llm-request-status [sessionId]="agentService.currentSessionId()" [running]="agentService.isCurrentSessionRunning()" (activeChange)="hasModelRequest.set($event)" />
|
||||
@if (agentService.isSessionContentLoading()) {
|
||||
<div class="session-loading-state" aria-live="polite">
|
||||
@@ -522,16 +370,16 @@ limitations under the License.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!agentService.isSessionContentLoading() && phases().length === 0 && !showPlanningLoader() && !hasModelRequest() && startupWorkItems().length === 0) {
|
||||
@if (!agentService.isSessionContentLoading() && phases().length === 0 && !showPlanningLoader() && !hasModelRequest() && startupWorkItems().length === 0 && awaitingGuidance().length === 0) {
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><span class="material-symbols-outlined">sensors</span></div>
|
||||
<h3>尚未选择执行记录</h3>
|
||||
<p>请从右侧任务列表选择会话,查看实时执行记录。</p>
|
||||
<h3>{{ agentService.currentSessionId() ? '暂无步骤记录' : '尚未选择执行记录' }}</h3>
|
||||
<p>{{ agentService.currentSessionId() ? '此任务尚未留下执行步骤。' : '请从右侧任务列表选择会话,查看实时执行记录。' }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- The phase-based session logs list -->
|
||||
@if (!agentService.isSessionContentLoading() && (phases().length > 0 || showPlanningLoader() || startupWorkItems().length > 0)) {
|
||||
@if (!agentService.isSessionContentLoading() && (phases().length > 0 || showPlanningLoader() || startupWorkItems().length > 0 || awaitingGuidance().length > 0)) {
|
||||
<div class="logs-list-container">
|
||||
@if (startupWorkItems().length > 0) {
|
||||
<div class="phase-container startup-phase" aria-live="polite">
|
||||
@@ -1230,10 +1078,10 @@ limitations under the License.
|
||||
<span class="grid-value code-pill">{{ param.value }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (plugin.result) {
|
||||
@if (plugin.result || plugin.rawResult || plugin.facts.length) {
|
||||
<div class="grid-item full-width">
|
||||
<span class="grid-label">实际结果</span>
|
||||
<span class="grid-value plugin-result">{{ plugin.result }}</span>
|
||||
<app-plugin-tool-result [view]="plugin" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1346,10 +1194,11 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
@if (!conversationMessages().length && awaitingGuidance().length) {
|
||||
<div class="guidance-queue" aria-label="待处理用户指导">
|
||||
<div class="guidance-queue" aria-label="指导接收与消费记录">
|
||||
@for (item of awaitingGuidance(); track item.id) {
|
||||
<div class="guidance-queue-item">
|
||||
<strong>用户指导 · {{ guidanceStatusLabel(item) }}</strong>
|
||||
<small>接收 {{ (item.accepted_at || item.timestamp) * 1000 | date:'HH:mm:ss' }}@if (item.consumed_at) { · 执行器读取 {{ item.consumed_at * 1000 | date:'HH:mm:ss' }} }</small>
|
||||
<p>{{ item.instruction }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,9 @@ import { mergeConversation } from '../../utils/exploration-timeline.util';
|
||||
import { TaskIdCopyComponent } from '../task-id-copy/task-id-copy.component';
|
||||
import { TaskHistoryComponent } from '../task-history/task-history.component';
|
||||
import { LlmRequestStatusComponent } from '../llm-request-status/llm-request-status.component';
|
||||
import { TaskLifecycleComponent } from '../task-lifecycle/task-lifecycle.component';
|
||||
import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
|
||||
import { PluginToolResultComponent } from '../plugin-tool-result/plugin-tool-result.component';
|
||||
import { AutomationPanelComponent } from '../automation-panel/automation-panel.component';
|
||||
import { UiLabelPipe } from '../../utils/ui-label.pipe';
|
||||
/**
|
||||
@@ -248,7 +250,7 @@ export type { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote, StreamRe
|
||||
@Component({
|
||||
selector: 'app-agent-stream',
|
||||
standalone: true,
|
||||
imports: [ReportMarkdownComponent, LlmRequestStatusComponent, TaskIdCopyComponent, UiLabelPipe, CommonModule, FormsModule, OverlayModule, TaskHistoryComponent, AutomationPanelComponent, TestResultBadgeComponent, TestResultCardComponent, TaskBatchFilterComponent],
|
||||
imports: [PluginToolResultComponent, ReportMarkdownComponent, LlmRequestStatusComponent, TaskLifecycleComponent, TaskIdCopyComponent, UiLabelPipe, CommonModule, FormsModule, OverlayModule, TaskHistoryComponent, AutomationPanelComponent, TestResultBadgeComponent, TestResultCardComponent, TaskBatchFilterComponent],
|
||||
templateUrl: './agent-stream.component.html',
|
||||
styleUrl: './agent-stream.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -504,12 +506,13 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
});
|
||||
|
||||
public guidanceStatusLabel(event: GuidanceEvent): string {
|
||||
const terminal = ['completed', 'failed', 'cancelled', 'interrupted']
|
||||
const terminal = ['completed', 'success', 'failed', 'cancelled', 'interrupted', 'skipped']
|
||||
.includes(this.agentService.currentSession()?.status || '');
|
||||
if (event.status === 'pending') {
|
||||
return terminal ? '任务结束前未进入模型' : '已接收,正在打断当前模型步骤';
|
||||
}
|
||||
return terminal ? '模型已读取,未留下后续动作记录' : '模型已读取,等待动作记录';
|
||||
if (event.status === 'unapplied' || (event.status === 'pending' && terminal)) return '任务结束,未记录消费';
|
||||
if (event.status === 'unconfirmed' || (event.status === 'reading' && terminal)) return '任务结束,读取未确认';
|
||||
if (event.status === 'reading') return '执行器正在读取';
|
||||
if (event.status === 'pending') return '已接收,等待安全时点读取';
|
||||
return terminal ? '执行器已读取,未留下后续动作记录' : '执行器已读取,等待动作记录';
|
||||
}
|
||||
|
||||
public phases = computed(() => {
|
||||
@@ -959,6 +962,11 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
public taskPhaseLabel(session: Session): string | null {
|
||||
if (['completed', 'failed', 'cancelled', 'interrupted', 'skipped'].includes(this.getTaskStatus(session))) return null;
|
||||
return ({ waiting_resources: '等待资源', paused: '已暂停', demonstrating: '人工示范中', analyzing_demo: '分析示范中' } as Record<string, string>)[session.phase || ''] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the device serial number for the session
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
@if (r.error) { <div class="report-error" role="status"><strong>异常原因</strong><p>{{r.error}}</p></div> }
|
||||
</div>
|
||||
<div class="report-meta"><div><strong>任务目标与预期</strong><p>{{r.goal || '未记录'}}</p></div>@if(r.expected){<div><strong>预期结果</strong><p>{{r.expected}}</p></div>}</div>
|
||||
<app-evidence-sources [value]="r.evidence_sources || null" />
|
||||
<div class="actions"><a [href]="'/api/automation/reports/'+r.id+'/docx'">下载 Word</a><a [href]="'/api/automation/reports/'+r.id+'/xlsx'">下载 Excel</a>
|
||||
<a [href]="'/api/automation/reports/'+r.id+'/screenshots'">步骤截图 ZIP</a>
|
||||
<a [href]="'/api/automation/reports/'+r.id+'/video'">分段录屏 ZIP</a>
|
||||
|
||||
+45
-45
@@ -1,60 +1,60 @@
|
||||
:host { display:block;min-width:0;color:#1e293b;font-size:13px;line-height:1.65; }
|
||||
:host { display:block;min-width:0;color:var(--app-text-control, #1e293b);font-size:13px;line-height:1.65; }
|
||||
.back-to-batch{margin-bottom:12px;max-width:100%;text-align:left;overflow-wrap:anywhere}
|
||||
.job.selected{background:#eff6ff;border-radius:8px;padding-left:8px;padding-right:8px}.job.selected button{color:#1d4ed8;border-color:#93c5fd;background:#fff}
|
||||
.tabs {display:flex;gap:4px;padding:4px;margin-bottom:18px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:20px;
|
||||
button {flex:1;padding:7px 8px;border:0;border-radius:16px;color:#475569;background:transparent;white-space:nowrap;
|
||||
&.selected {color:#2563eb;background:#fff;box-shadow:0 1px 4px #0f172a12;} }
|
||||
.job.selected{background:var(--app-primary-soft, #eff6ff);border-radius:8px;padding-left:8px;padding-right:8px}.job.selected button{color:var(--app-primary-text, #1d4ed8);border-color:var(--app-primary-border, #93c5fd);background:var(--app-surface, #fff)}
|
||||
.tabs {display:flex;gap:4px;padding:4px;margin-bottom:18px;background:var(--app-surface-soft, #f1f5f9);border:1px solid var(--app-border, #e2e8f0);border-radius:20px;
|
||||
button {flex:1;padding:7px 8px;border:0;border-radius:16px;color:var(--app-text-secondary, #475569);background:transparent;white-space:nowrap;
|
||||
&.selected {color:var(--app-primary-text, #2563eb);background:var(--app-surface, #fff);box-shadow:0 1px 4px #0f172a12;} }
|
||||
}
|
||||
section {padding:16px;margin-bottom:14px;border:1px solid #e2e8f0;border-radius:12px;background:#fff;&:last-child{margin-bottom:0;}}
|
||||
h3 {color:#0f172a;font-size:14px;font-weight:600;margin:0 0 8px;}
|
||||
p {margin:8px 0 12px;color:#475569;overflow-wrap:anywhere;}strong{font-weight:600;}.muted,small{color:#64748b;font-size:12px;}
|
||||
.error,.notice{padding:10px 12px;border-radius:8px;margin:0 0 14px;}.error{color:#b91c1c;background:#fff1f2;border:1px solid #fecdd3;}.notice{color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;}
|
||||
button,a,.upload {display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:34px;border:1px solid #cbd5e1;border-radius:8px;padding:7px 12px;background:#fff;color:#334155;text-decoration:none;cursor:pointer;font:inherit;font-weight:500;line-height:1.5;transition:background .15s,border-color .15s;
|
||||
&:hover{background:#f8fafc;border-color:#94a3b8;}&:focus-visible{outline:2px solid #3b82f6;outline-offset:2px;}&:disabled{color:#94a3b8;background:#f8fafc;border-color:#e2e8f0;cursor:not-allowed;}}
|
||||
a{color:#2563eb;border-color:#bfdbfe;background:#eff6ff;}button.primary{background:#2563eb;color:white;border-color:#2563eb;width:100%;margin-top:10px;}button.primary:hover{background:#1d4ed8;}button.primary:disabled{background:#e2e8f0;color:#64748b;border-color:#e2e8f0;}
|
||||
.actions,.fields,.format-options{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}.actions{margin:12px 0 4px;}.fields>*{flex:1;min-width:130px;}.format-options{margin:12px 0;}.format-options label{flex:1;min-width:120px;margin:0;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;}
|
||||
label{display:block;margin:12px 0;color:#334155;font-weight:500;}
|
||||
input:not([type=checkbox]):not([type=file]),select,textarea{box-sizing:border-box;display:block;width:100%;margin-top:6px;border:1px solid #cbd5e1;border-radius:8px;padding:9px 10px;background:#fff;color:#1e293b;font:inherit;font-weight:400;&:focus{outline:2px solid #bfdbfe;border-color:#3b82f6;}}
|
||||
input[type=checkbox]{width:15px;height:15px;accent-color:#2563eb;vertical-align:-3px;margin-right:6px;}
|
||||
section {padding:16px;margin-bottom:14px;border:1px solid var(--app-border, #e2e8f0);border-radius:12px;background:var(--app-surface, #fff);&:last-child{margin-bottom:0;}}
|
||||
h3 {color:var(--app-text-strong, #0f172a);font-size:14px;font-weight:600;margin:0 0 8px;}
|
||||
p {margin:8px 0 12px;color:var(--app-text-secondary, #475569);overflow-wrap:anywhere;}strong{font-weight:600;}.muted,small{color:var(--app-text-muted, #64748b);font-size:12px;}
|
||||
.error,.notice{padding:10px 12px;border-radius:8px;margin:0 0 14px;}.error{color:var(--app-error-text, #b91c1c);background:var(--app-error-soft, #fff1f2);border:1px solid var(--app-error-border, #fecdd3);}.notice{color:var(--app-success-text, #047857);background:var(--app-success-soft, #ecfdf5);border:1px solid var(--app-success-border, #a7f3d0);}
|
||||
button,a,.upload {display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:34px;border:1px solid var(--app-border-control, #cbd5e1);border-radius:8px;padding:7px 12px;background:var(--app-surface, #fff);color:var(--app-text-control, #334155);text-decoration:none;cursor:pointer;font:inherit;font-weight:500;line-height:1.5;transition:background .15s,border-color .15s;
|
||||
&:hover{background:var(--app-surface-soft, #f8fafc);border-color:var(--app-border-control, #94a3b8);}&:focus-visible{outline:2px solid #3b82f6;outline-offset:2px;}&:disabled{color:var(--app-text-faint, #94a3b8);background:var(--app-surface-soft, #f8fafc);border-color:var(--app-border, #e2e8f0);cursor:not-allowed;}}
|
||||
a{color:var(--app-primary-text, #2563eb);border-color:var(--app-primary-border, #bfdbfe);background:var(--app-primary-soft, #eff6ff);}button.primary{background:#2563eb;color:white;border-color:#2563eb;width:100%;margin-top:10px;}button.primary:hover{background:#1d4ed8;}button.primary:disabled{background:var(--app-surface-raised, #e2e8f0);color:var(--app-text-muted, #64748b);border-color:var(--app-border, #e2e8f0);}
|
||||
.actions,.fields,.format-options{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}.actions{margin:12px 0 4px;}.fields>*{flex:1;min-width:130px;}.format-options{margin:12px 0;}.format-options label{flex:1;min-width:120px;margin:0;padding:10px 12px;border:1px solid var(--app-border, #e2e8f0);border-radius:8px;background:var(--app-surface-soft, #f8fafc);}
|
||||
label{display:block;margin:12px 0;color:var(--app-text-control, #334155);font-weight:500;}
|
||||
input:not([type=checkbox]):not([type=file]),select,textarea{box-sizing:border-box;display:block;width:100%;margin-top:6px;border:1px solid var(--app-border-control, #cbd5e1);border-radius:8px;padding:9px 10px;background:var(--app-surface, #fff);color:var(--app-text-control, #1e293b);font:inherit;font-weight:400;&:focus{outline:2px solid var(--app-primary-border, #bfdbfe);border-color:#3b82f6;}}
|
||||
input[type=checkbox]{width:15px;height:15px;accent-color:var(--app-primary-text, #2563eb);vertical-align:-3px;margin-right:6px;}
|
||||
.upload{position:relative;margin:0;color:#fff;background:#2563eb;border-color:#2563eb;}.upload:hover{background:#1d4ed8;border-color:#1d4ed8;}.upload input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;}
|
||||
.device{background:#f8fafc;padding:12px 16px;}.device label{margin:0;}
|
||||
.selection-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:12px 0;color:#475569;}
|
||||
.device{background:var(--app-surface-soft, #f8fafc);padding:12px 16px;}.device label{margin:0;}
|
||||
.selection-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:12px 0;color:var(--app-text-secondary, #475569);}
|
||||
.selection-column{flex:0 0 18px;text-align:center;}.selection-column input{margin:0;}
|
||||
.case-preview-list{max-height:320px;overflow:auto;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:14px;}
|
||||
.case-preview{display:flex;align-items:flex-start;gap:10px;padding:12px;border-bottom:1px solid #e2e8f0;&:last-child{border-bottom:0;}&.unselected{background:#f8fafc;color:#64748b;}}
|
||||
.case-preview-content{min-width:0;flex:1;strong{display:block;overflow-wrap:anywhere;}small{display:block;}details{margin:5px 0 0;}summary{padding:0;font-size:12px;color:#2563eb;}pre{max-height:160px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:#f8fafc;border-radius:6px;padding:8px;font-size:11px;}}
|
||||
.case-preview-list{max-height:320px;overflow:auto;border:1px solid var(--app-border, #e2e8f0);border-radius:8px;margin-bottom:14px;}
|
||||
.case-preview{display:flex;align-items:flex-start;gap:10px;padding:12px;border-bottom:1px solid var(--app-border, #e2e8f0);&:last-child{border-bottom:0;}&.unselected{background:var(--app-surface-soft, #f8fafc);color:var(--app-text-muted, #64748b);}}
|
||||
.case-preview-content{min-width:0;flex:1;strong{display:block;overflow-wrap:anywhere;}small{display:block;}details{margin:5px 0 0;}summary{padding:0;font-size:12px;color:var(--app-primary-text, #2563eb);}pre{max-height:160px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:var(--app-surface-soft, #f8fafc);border-radius:6px;padding:8px;font-size:11px;}}
|
||||
.case-goal{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;white-space:pre-wrap;margin:5px 0;font-size:12px;}
|
||||
.execution-summary{padding:9px 12px;border-radius:8px;background:#eff6ff;color:#1d4ed8;font-weight:500;}
|
||||
.record{display:flex;width:100%;text-align:left;align-items:flex-start;flex-direction:column;gap:4px;margin:8px 0 0;padding:12px;overflow-wrap:anywhere;}.record span{color:#64748b;font-size:12px;font-weight:400;}.record.selected{border-color:#93c5fd;background:#eff6ff;}.record.selected strong{color:#1d4ed8;}
|
||||
.batch-record{position:relative;display:flex;gap:8px;align-items:stretch;margin-top:8px;}.batch-record>app-favorite-button{position:absolute;left:9px;top:9px;z-index:1}.batch-record .record{padding-left:44px;flex:1;min-width:0;width:auto;margin:0;}.batch-record .delete-batch{flex:0 0 40px;min-width:40px;padding:0;color:#b91c1c;}.batch-record .delete-batch:hover:not(:disabled){background:#fff1f2;border-color:#fda4af;}.batch-record .delete-batch .material-symbols-outlined{font-size:19px;}
|
||||
.archive-progress{margin:12px 0;padding:11px 13px;border:1px solid #bfdbfe;border-radius:10px;background:#f8fbff;color:#1e40af;}
|
||||
.execution-summary{padding:9px 12px;border-radius:8px;background:var(--app-primary-soft, #eff6ff);color:var(--app-primary-text, #1d4ed8);font-weight:500;}
|
||||
.record{display:flex;width:100%;text-align:left;align-items:flex-start;flex-direction:column;gap:4px;margin:8px 0 0;padding:12px;overflow-wrap:anywhere;}.record span{color:var(--app-text-muted, #64748b);font-size:12px;font-weight:400;}.record.selected{border-color:var(--app-primary-border, #93c5fd);background:var(--app-primary-soft, #eff6ff);}.record.selected strong{color:var(--app-primary-text, #1d4ed8);}
|
||||
.batch-record{position:relative;display:flex;gap:8px;align-items:stretch;margin-top:8px;}.batch-record>app-favorite-button{position:absolute;left:9px;top:9px;z-index:1}.batch-record .record{padding-left:44px;flex:1;min-width:0;width:auto;margin:0;}.batch-record .delete-batch{flex:0 0 40px;min-width:40px;padding:0;color:var(--app-error-text, #b91c1c);}.batch-record .delete-batch:hover:not(:disabled){background:var(--app-error-soft, #fff1f2);border-color:var(--app-error-border, #fda4af);}.batch-record .delete-batch .material-symbols-outlined{font-size:19px;}
|
||||
.archive-progress{margin:12px 0;padding:11px 13px;border:1px solid var(--app-primary-border, #bfdbfe);border-radius:10px;background:var(--app-primary-soft, #f8fbff);color:var(--app-primary-text, #1e40af);}
|
||||
.archive-progress-heading{display:flex;align-items:center;gap:8px;}
|
||||
.archive-progress p{margin:7px 0 0;font-size:12px;color:#475569;}
|
||||
.archive-progress.archive-failed{border-color:#fecdd3;background:#fff1f2;color:#be123c;}
|
||||
.archive-track{height:7px;margin-top:10px;border-radius:999px;background:#dbeafe;overflow:hidden;}
|
||||
.archive-progress p{margin:7px 0 0;font-size:12px;color:var(--app-text-secondary, #475569);}
|
||||
.archive-progress.archive-failed{border-color:var(--app-error-border, #fecdd3);background:var(--app-error-soft, #fff1f2);color:var(--app-error-text, #be123c);}
|
||||
.archive-track{height:7px;margin-top:10px;border-radius:999px;background:var(--app-primary-soft, #dbeafe);overflow:hidden;}
|
||||
.archive-track span{display:block;height:100%;border-radius:inherit;background:#3b82f6;transition:width .3s ease;}
|
||||
.archive-spinner{width:13px;height:13px;border:2px solid #bfdbfe;border-top-color:#2563eb;border-radius:50%;animation:archive-spin .8s linear infinite;}
|
||||
.archive-spinner{width:13px;height:13px;border:2px solid var(--app-primary-border, #bfdbfe);border-top-color:#2563eb;border-radius:50%;animation:archive-spin .8s linear infinite;}
|
||||
@keyframes archive-spin{to{transform:rotate(360deg);}}
|
||||
.code{font-family:Consolas,monospace;font-size:12px;line-height:1.6;resize:vertical;background:#f8fafc;}.table-wrap{max-height:260px;overflow:auto;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:14px;}table{width:100%;border-collapse:collapse;}th,td{padding:9px 10px;border-bottom:1px solid #e2e8f0;text-align:left;overflow-wrap:anywhere;}th{background:#f8fafc;color:#475569;font-weight:500;}
|
||||
.preview{white-space:pre-wrap;max-height:400px;overflow:auto;font:inherit;color:#334155;background:#f8fafc;border-radius:8px;padding:12px;border:1px solid #e2e8f0;}
|
||||
.job{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:10px 0;border-bottom:1px solid #f1f5f9;}.job:last-child{border-bottom:0;}.job button{flex:1;justify-content:flex-start;text-align:left;min-width:150px;}.job span{font-size:12px;background:#f1f5f9;color:#475569;border-radius:5px;padding:3px 7px;}.job small{width:100%;}details{margin:14px 0;}summary{color:#475569;cursor:pointer;padding:8px 0;}
|
||||
.task-guidance{margin-top:16px;padding:14px;border:1px solid #bfdbfe;border-radius:10px;background:#f8fbff;}
|
||||
.code{font-family:Consolas,monospace;font-size:12px;line-height:1.6;resize:vertical;background:var(--app-surface-soft, #f8fafc);}.table-wrap{max-height:260px;overflow:auto;border:1px solid var(--app-border, #e2e8f0);border-radius:8px;margin-bottom:14px;}table{width:100%;border-collapse:collapse;}th,td{padding:9px 10px;border-bottom:1px solid var(--app-border, #e2e8f0);text-align:left;overflow-wrap:anywhere;}th{background:var(--app-surface-soft, #f8fafc);color:var(--app-text-secondary, #475569);font-weight:500;}
|
||||
.preview{white-space:pre-wrap;max-height:400px;overflow:auto;font:inherit;color:var(--app-text-control, #334155);background:var(--app-surface-soft, #f8fafc);border-radius:8px;padding:12px;border:1px solid var(--app-border, #e2e8f0);}
|
||||
.job{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:10px 0;border-bottom:1px solid var(--app-border, #f1f5f9);}.job:last-child{border-bottom:0;}.job button{flex:1;justify-content:flex-start;text-align:left;min-width:150px;}.job span{font-size:12px;background:var(--app-surface-soft, #f1f5f9);color:var(--app-text-secondary, #475569);border-radius:5px;padding:3px 7px;}.job small{width:100%;}details{margin:14px 0;}summary{color:var(--app-text-secondary, #475569);cursor:pointer;padding:8px 0;}
|
||||
.task-guidance{margin-top:16px;padding:14px;border:1px solid var(--app-primary-border, #bfdbfe);border-radius:10px;background:var(--app-primary-soft, #f8fbff);}
|
||||
.task-guidance label{margin:8px 0 4px;}
|
||||
.report-overview{padding:14px;border:1px solid #dbeafe;border-radius:10px;background:#f8fbff;}
|
||||
.report-overview{padding:14px;border:1px solid var(--app-primary-border, #dbeafe);border-radius:10px;background:var(--app-primary-soft, #f8fbff);}
|
||||
.report-heading{display:flex;align-items:flex-start;gap:8px;}.report-heading .report-title{flex:1;min-width:0;}
|
||||
.report-title{display:block;font-size:14px;line-height:1.5;color:#0f172a;overflow-wrap:anywhere;}
|
||||
.report-badges{display:flex;gap:7px;flex-wrap:wrap;margin-top:9px;span{padding:3px 8px;border:1px solid #bfdbfe;border-radius:20px;background:#eff6ff;color:#1e40af;font-size:12px;&.failed{background:#fff1f2;border-color:#fecdd3;color:#be123c;}}}
|
||||
.report-error{margin-top:11px;padding:10px 12px;background:#fff1f2;border-left:3px solid #e11d48;border-radius:6px;color:#9f1239;strong{font-size:12px;}p{margin:4px 0 0;color:#9f1239;white-space:pre-wrap;}}
|
||||
.report-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:12px 0;div{min-width:0;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;}strong{font-size:12px;color:#475569;}p{margin:5px 0 0;white-space:pre-wrap;color:#1e293b;}}
|
||||
.report-content{margin-top:14px;padding:14px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;h3{margin-bottom:12px;}}
|
||||
.report-body{overflow-x:auto;overflow-wrap:anywhere;color:#1e293b;line-height:1.75;}
|
||||
.report-title{display:block;font-size:14px;line-height:1.5;color:var(--app-text-strong, #0f172a);overflow-wrap:anywhere;}
|
||||
.report-badges{display:flex;gap:7px;flex-wrap:wrap;margin-top:9px;span{padding:3px 8px;border:1px solid var(--app-primary-border, #bfdbfe);border-radius:20px;background:var(--app-primary-soft, #eff6ff);color:var(--app-primary-text, #1e40af);font-size:12px;&.failed{background:var(--app-error-soft, #fff1f2);border-color:var(--app-error-border, #fecdd3);color:var(--app-error-text, #be123c);}}}
|
||||
.report-error{margin-top:11px;padding:10px 12px;background:var(--app-error-soft, #fff1f2);border-left:3px solid #e11d48;border-radius:6px;color:var(--app-error-text, #9f1239);strong{font-size:12px;}p{margin:4px 0 0;color:var(--app-error-text, #9f1239);white-space:pre-wrap;}}
|
||||
.report-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:12px 0;div{min-width:0;padding:10px 12px;border:1px solid var(--app-border, #e2e8f0);border-radius:8px;background:var(--app-surface, #fff);}strong{font-size:12px;color:var(--app-text-secondary, #475569);}p{margin:5px 0 0;white-space:pre-wrap;color:var(--app-text-control, #1e293b);}}
|
||||
.report-content{margin-top:14px;padding:14px;border:1px solid var(--app-border, #e2e8f0);border-radius:10px;background:var(--app-surface, #fff);h3{margin-bottom:12px;}}
|
||||
.report-body{overflow-x:auto;overflow-wrap:anywhere;color:var(--app-text-control, #1e293b);line-height:1.75;}
|
||||
:host ::ng-deep .report-body, :host ::ng-deep .report-markdown-body {
|
||||
h1,h2,h3,h4{margin:18px 0 8px;color:#0f172a;line-height:1.4;}h1{font-size:18px;}h2{font-size:16px;}h3,h4{font-size:14px;}
|
||||
h1,h2,h3,h4{margin:18px 0 8px;color:var(--app-text-strong, #0f172a);line-height:1.4;}h1{font-size:18px;}h2{font-size:16px;}h3,h4{font-size:14px;}
|
||||
> div{margin:3px 0;}
|
||||
ul,ol{margin:8px 0;padding-left:24px;}li{margin:4px 0;}
|
||||
blockquote{margin:10px 0;padding:8px 12px;border-left:3px solid #93c5fd;background:#f8fafc;}
|
||||
pre{max-width:100%;overflow:auto;padding:10px 12px;background:#f1f5f9;border-radius:6px;white-space:pre-wrap;}
|
||||
code{font-family:Consolas,monospace;background:#f1f5f9;padding:1px 3px;border-radius:3px;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12px;}th,td{padding:7px 9px;border:1px solid #e2e8f0;vertical-align:top;}th{background:#eff6ff;color:#1e3a8a;}
|
||||
blockquote{margin:10px 0;padding:8px 12px;border-left:3px solid var(--app-primary-border, #93c5fd);background:var(--app-surface-soft, #f8fafc);}
|
||||
pre{max-width:100%;overflow:auto;padding:10px 12px;background:var(--app-surface-soft, #f1f5f9);border-radius:6px;white-space:pre-wrap;}
|
||||
code{font-family:Consolas,monospace;background:var(--app-surface-soft, #f1f5f9);padding:1px 3px;border-radius:3px;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12px;}th,td{padding:7px 9px;border:1px solid var(--app-border, #e2e8f0);vertical-align:top;}th{background:var(--app-primary-soft, #eff6ff);color:var(--app-primary-text, #1e3a8a);}
|
||||
.md-para-gap{height:8px;}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { TestResultBadgeComponent } from '../test-result-badge/test-result-badge
|
||||
import { TestResultCardComponent } from '../test-result-badge/test-result-card.component';
|
||||
import { CaseReviewResponse } from '../../core/models/session.model';
|
||||
import { TaskPluginsComponent, TaskPluginOptions } from '../task-plugins/task-plugins.component';
|
||||
import { EvidenceSourcesComponent } from '../evidence-sources/evidence-sources.component';
|
||||
|
||||
interface ArchiveProgress {
|
||||
status: 'idle' | 'preparing' | 'ready' | 'failed';
|
||||
@@ -23,7 +24,7 @@ interface ArchiveProgress {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-automation-panel', standalone: true, imports: [FavoriteButtonComponent, ReportMarkdownComponent, CommonModule, FormsModule, TestResultBadgeComponent, TestResultCardComponent, TaskPluginsComponent],
|
||||
selector: 'app-automation-panel', standalone: true, imports: [FavoriteButtonComponent, ReportMarkdownComponent, CommonModule, FormsModule, TestResultBadgeComponent, TestResultCardComponent, TaskPluginsComponent, EvidenceSourcesComponent],
|
||||
templateUrl: './automation-panel.component.html', styleUrl: './automation-panel.component.scss'
|
||||
})
|
||||
export class AutomationPanelComponent {
|
||||
|
||||
+124
-124
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
$border-color: rgba(0, 0, 0, 0.08);
|
||||
$text-primary: #1f1f1f;
|
||||
$text-secondary: #71717a;
|
||||
$text-muted: #a1a1aa;
|
||||
$text-primary: var(--app-text-strong, #1f1f1f);
|
||||
$text-secondary: var(--app-text-muted, #71717a);
|
||||
$text-muted: var(--app-text-faint, #a1a1aa);
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -32,7 +32,7 @@ $text-muted: #a1a1aa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
box-sizing: border-box;
|
||||
|
||||
.chat-header {
|
||||
@@ -43,23 +43,23 @@ $text-muted: #a1a1aa;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
box-sizing: border-box;
|
||||
|
||||
.tab-selector-bar {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(241, 245, 249, 0.72);
|
||||
background: rgb(var(--app-surface-soft-rgb, 241 245 249) / 0.72);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.85);
|
||||
border-radius: 18px;
|
||||
padding: 3px;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.8);
|
||||
|
||||
.tab-selector-btn {
|
||||
display: inline-flex;
|
||||
@@ -74,7 +74,7 @@ $text-muted: #a1a1aa;
|
||||
border-radius: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -90,13 +90,13 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #0f172a;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.7);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #2563eb;
|
||||
background: var(--app-surface, #ffffff);
|
||||
color: var(--app-primary-text, #2563eb);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ $text-muted: #a1a1aa;
|
||||
background: linear-gradient(135deg, rgba(37, 99, 235, 0.92), rgba(26, 115, 232, 0.85));
|
||||
backdrop-filter: blur(14px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 255 255 255) / 0.35);
|
||||
border-radius: 16px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
@@ -127,7 +127,7 @@ $text-muted: #a1a1aa;
|
||||
gap: 5px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-right: 8px;
|
||||
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.45);
|
||||
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.3), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.45);
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 16px;
|
||||
@@ -136,8 +136,8 @@ $text-muted: #a1a1aa;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, rgba(37, 99, 235, 1), rgba(26, 115, 232, 0.95));
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.42), inset 0 1px 1px rgba(255, 255, 255, 0.6);
|
||||
border-color: rgb(var(--app-border-rgb, 255 255 255) / 0.6);
|
||||
box-shadow: 0 6px 18px rgba(37, 99, 235, 0.42), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.6);
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
|
||||
@@ -151,17 +151,17 @@ $text-muted: #a1a1aa;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.72);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
border: 1px solid rgba(203, 213, 225, 0.85);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 203 213 225) / 0.85);
|
||||
border-radius: 50%;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 2px 6px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.9);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.material-symbols-outlined {
|
||||
@@ -170,9 +170,9 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
border-color: #fca5a5;
|
||||
background: var(--app-error-soft, #fef2f2);
|
||||
color: var(--app-error-text, #ef4444);
|
||||
border-color: var(--app-error-border, #fca5a5);
|
||||
box-shadow: 0 3px 10px rgba(239, 68, 68, 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
@@ -192,7 +192,7 @@ $text-muted: #a1a1aa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
@@ -205,7 +205,7 @@ $text-muted: #a1a1aa;
|
||||
.queue-section-header {
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
margin: 6px 0 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -221,18 +221,18 @@ $text-muted: #a1a1aa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f1f5f9;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
border: 1px dashed var(--app-border-control, #cbd5e1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid rgba(226, 232, 240, 0.8);
|
||||
border-top: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.8);
|
||||
padding-top: 12px;
|
||||
|
||||
&[open] .history-summary {
|
||||
@@ -242,7 +242,7 @@ $text-muted: #a1a1aa;
|
||||
.history-summary {
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
@@ -265,8 +265,8 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
color: var(--app-text-control, #1e293b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +278,8 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
background: var(--app-surface, #ffffff);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.8);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
@@ -292,7 +292,7 @@ $text-muted: #a1a1aa;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(203, 213, 225, 1);
|
||||
border-color: rgb(var(--app-border-control-rgb, 203 213 225) / 1);
|
||||
box-shadow: 0 3px 8px rgba(148, 163, 184, 0.08);
|
||||
|
||||
.task-card-header .task-card-header-actions {
|
||||
@@ -314,7 +314,7 @@ $text-muted: #a1a1aa;
|
||||
&.selected {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 1px #3b82f6, 0 4px 12px rgba(59, 130, 246, 0.08);
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
}
|
||||
|
||||
&.running {
|
||||
@@ -336,7 +336,7 @@ $text-muted: #a1a1aa;
|
||||
min-width: 0;
|
||||
|
||||
.task-status-group { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
|
||||
.task-origin-badge { display: inline-flex; align-items: center; box-sizing: border-box; height: 20px; padding: 0 6px; border: 1px solid #e2e8f0; border-radius: 5px; background: #f8fafc; color: #64748b; font-size: 11px; line-height: 14px; white-space: nowrap; }
|
||||
.task-origin-badge { display: inline-flex; align-items: center; box-sizing: border-box; height: 20px; padding: 0 6px; border: 1px solid var(--app-border, #e2e8f0); border-radius: 5px; background: var(--app-surface-soft, #f8fafc); color: var(--app-text-muted, #64748b); font-size: 11px; line-height: 14px; white-space: nowrap; }
|
||||
|
||||
.task-badge {
|
||||
flex-shrink: 0;
|
||||
@@ -350,9 +350,9 @@ $text-muted: #a1a1aa;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.05em;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fafafa;
|
||||
color: #64748b;
|
||||
border: 1px solid var(--app-border, #e2e8f0);
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
color: var(--app-text-muted, #64748b);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
@@ -364,48 +364,48 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&.running {
|
||||
background: #fafafa;
|
||||
color: #1f1f1f;
|
||||
border-color: #1f1f1f;
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
color: var(--app-text-strong, #1f1f1f);
|
||||
border-color: var(--app-border-control, #1f1f1f);
|
||||
&::before {
|
||||
background-color: #1f1f1f;
|
||||
animation: pulse-black-dot-small 1.5s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
&.paused {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
border-color: #fdba74;
|
||||
background: var(--app-warning-soft, #fff7ed);
|
||||
color: var(--app-warning-text, #c2410c);
|
||||
border-color: var(--app-warning-border, #fdba74);
|
||||
&::before { background-color: #f97316; }
|
||||
}
|
||||
&.completed {
|
||||
background: #fafafa;
|
||||
color: #1f1f1f;
|
||||
border-color: #e2e8f0;
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
color: var(--app-text-strong, #1f1f1f);
|
||||
border-color: var(--app-border, #e2e8f0);
|
||||
&::before {
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
}
|
||||
&.pending {
|
||||
background: #fafafa;
|
||||
color: #64748b;
|
||||
border-color: #e2e8f0;
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
color: var(--app-text-muted, #64748b);
|
||||
border-color: var(--app-border, #e2e8f0);
|
||||
&::before {
|
||||
background-color: #94a3b8;
|
||||
}
|
||||
}
|
||||
&.failed {
|
||||
background: #fafafa;
|
||||
color: #ef4444;
|
||||
border-color: #fee2e2;
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
color: var(--app-error-text, #ef4444);
|
||||
border-color: var(--app-error-border, #fee2e2);
|
||||
&::before {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
}
|
||||
&.cancelled {
|
||||
background: #fafafa;
|
||||
color: #d97706;
|
||||
border-color: #fef3c7;
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
color: var(--app-warning-text, #d97706);
|
||||
border-color: var(--app-warning-border, #fef3c7);
|
||||
&::before {
|
||||
background-color: #f59e0b;
|
||||
}
|
||||
@@ -433,7 +433,7 @@ $text-muted: #a1a1aa;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
line-height: 1;
|
||||
cursor: default;
|
||||
transition: color 0.15s ease, opacity 0.15s ease;
|
||||
@@ -443,7 +443,7 @@ $text-muted: #a1a1aa;
|
||||
width: 12.5px;
|
||||
height: 12.5px;
|
||||
line-height: 12.5px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
opacity: 0.75;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -458,9 +458,9 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
.device-icon {
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -477,7 +477,7 @@ $text-muted: #a1a1aa;
|
||||
|
||||
.task-time {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
@@ -495,7 +495,7 @@ $text-muted: #a1a1aa;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -510,8 +510,8 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #ef4444;
|
||||
background: #fee2e2;
|
||||
color: var(--app-error-text, #ef4444);
|
||||
background: var(--app-error-soft, #fee2e2);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -531,7 +531,7 @@ $text-muted: #a1a1aa;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: #ef4444;
|
||||
color: var(--app-error-text, #ef4444);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -546,8 +546,8 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
color: var(--app-error-text, #dc2626);
|
||||
background: var(--app-error-soft, #fee2e2);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
@@ -572,7 +572,7 @@ $text-muted: #a1a1aa;
|
||||
.task-goal {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
display: -webkit-box;
|
||||
@@ -587,15 +587,15 @@ $text-muted: #a1a1aa;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-top: 1px solid rgba(241, 245, 249, 0.8);
|
||||
border-top: 1px solid rgb(var(--app-border-rgb, 241 245 249) / 0.8);
|
||||
padding-top: 6px;
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
|
||||
.task-id {
|
||||
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -604,14 +604,14 @@ $text-muted: #a1a1aa;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
}
|
||||
}
|
||||
|
||||
.task-date {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,7 +635,7 @@ $text-muted: #a1a1aa;
|
||||
.selector-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@@ -649,12 +649,12 @@ $text-muted: #a1a1aa;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--app-border, #e2e8f0);
|
||||
background: var(--app-surface, #ffffff);
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
@@ -663,15 +663,15 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
color: #1e293b;
|
||||
background: var(--app-surface-soft, #f8fafc);
|
||||
border-color: var(--app-border-control, #cbd5e1);
|
||||
color: var(--app-text-control, #1e293b);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
background: var(--app-primary-soft, #eff6ff);
|
||||
border-color: var(--app-primary-border, #bfdbfe);
|
||||
color: var(--app-primary-text, #1d4ed8);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -694,8 +694,8 @@ $text-muted: #a1a1aa;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
border: 1px solid var(--app-border, #e2e8f0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -704,7 +704,7 @@ $text-muted: #a1a1aa;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 26px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,14 +712,14 @@ $text-muted: #a1a1aa;
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.empty-notes-subtitle {
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
line-height: 1.5;
|
||||
max-width: 240px;
|
||||
margin: 0;
|
||||
@@ -736,7 +736,7 @@ $text-muted: #a1a1aa;
|
||||
.note-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1f1f1f;
|
||||
color: var(--app-text-strong, #1f1f1f);
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
|
||||
@@ -744,8 +744,8 @@ $text-muted: #a1a1aa;
|
||||
|
||||
.code-segment {
|
||||
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
|
||||
background-color: #f1f3f4;
|
||||
color: #1967d2;
|
||||
background-color: var(--app-info-soft, #f1f3f4);
|
||||
color: var(--app-primary-text, #1967d2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
@@ -756,8 +756,8 @@ $text-muted: #a1a1aa;
|
||||
|
||||
.checked {
|
||||
.code-segment {
|
||||
color: #a1a1aa;
|
||||
background-color: #f8f9fa;
|
||||
color: var(--app-text-faint, #a1a1aa);
|
||||
background-color: var(--app-surface-soft, #f8f9fa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,7 +778,7 @@ $text-muted: #a1a1aa;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.milestone-text {
|
||||
color: #a1a1aa;
|
||||
color: var(--app-text-faint, #a1a1aa);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,11 +795,11 @@ $text-muted: #a1a1aa;
|
||||
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
|
||||
&.checked {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
|
||||
&.progress {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
|
||||
&.unchecked {
|
||||
@@ -809,7 +809,7 @@ $text-muted: #a1a1aa;
|
||||
|
||||
.milestone-text {
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
|
||||
color: #1f1f1f;
|
||||
color: var(--app-text-strong, #1f1f1f);
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
font-weight: 400;
|
||||
@@ -839,7 +839,7 @@ $text-muted: #a1a1aa;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
|
||||
color: #1f1f1f;
|
||||
color: var(--app-text-strong, #1f1f1f);
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
|
||||
@@ -848,7 +848,7 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
color: #a1a1aa;
|
||||
color: var(--app-text-faint, #a1a1aa);
|
||||
|
||||
.check-icon.checked {
|
||||
opacity: 0.7;
|
||||
@@ -862,11 +862,11 @@ $text-muted: #a1a1aa;
|
||||
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
|
||||
&.checked {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
|
||||
&.progress {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
|
||||
&.unchecked {
|
||||
@@ -875,7 +875,7 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
color: #9ca3af;
|
||||
color: var(--app-text-faint, #9ca3af);
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
padding-left: 2px;
|
||||
@@ -907,8 +907,8 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
.verification-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: var(--app-surface-soft, #f8fafc);
|
||||
border: 1px solid var(--app-border, #e2e8f0);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
@@ -931,10 +931,10 @@ $text-muted: #a1a1aa;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
|
||||
&.assert {
|
||||
color: #0284c7;
|
||||
color: var(--app-info-text, #0284c7);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -943,35 +943,35 @@ $text-muted: #a1a1aa;
|
||||
font-family: inherit;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
color: var(--app-text-control, #334155);
|
||||
word-break: break-word;
|
||||
|
||||
.bold-segment {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
}
|
||||
|
||||
.code-segment {
|
||||
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
padding: 1.5px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid var(--app-border, #e2e8f0);
|
||||
}
|
||||
}
|
||||
|
||||
&.finding {
|
||||
background: #fffbeb;
|
||||
border-color: #fef3c7;
|
||||
background: var(--app-warning-soft, #fffbeb);
|
||||
border-color: var(--app-warning-border, #fef3c7);
|
||||
|
||||
.card-header .card-tag {
|
||||
color: #b45309;
|
||||
color: var(--app-warning-text, #b45309);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
color: #92400e;
|
||||
color: var(--app-warning-text, #92400e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -988,7 +988,7 @@ $text-muted: #a1a1aa;
|
||||
margin-bottom: 14px;
|
||||
min-height: 22px;
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
|
||||
color: #1f1f1f;
|
||||
color: var(--app-text-strong, #1f1f1f);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
@@ -1001,7 +1001,7 @@ $text-muted: #a1a1aa;
|
||||
font-weight: 700;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--app-border, #e5e7eb);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -1020,17 +1020,17 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
color: #a1a1aa;
|
||||
color: var(--app-text-faint, #a1a1aa);
|
||||
|
||||
.check-icon {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&.progress {
|
||||
.check-icon {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,7 +1042,7 @@ $text-muted: #a1a1aa;
|
||||
|
||||
&.list-item {
|
||||
.bullet {
|
||||
color: #9ca3af;
|
||||
color: var(--app-text-faint, #9ca3af);
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { EvidenceSourcesComponent } from './evidence-sources.component';
|
||||
|
||||
describe('EvidenceSourcesComponent', () => {
|
||||
it('keeps supporting API data separate from the platform and retains actual record identifiers', () => {
|
||||
const fixture = TestBed.createComponent(EvidenceSourcesComponent);
|
||||
fixture.componentRef.setInput('value', {total_sources:1, sources_omitted:0, unattributed_step_count:2,
|
||||
note:'界面结果不能单独证明后端持久状态。', sources:[{
|
||||
id:'src', category:'auxiliary', kind:'api', label:'安卓业务后台', target_id:'service', plugin_id:'data',
|
||||
reference_count:1, references:[{step_number:7, step_id:'actual-step', call_id:'actual-call'}]
|
||||
}]});
|
||||
fixture.detectChanges();
|
||||
const text = fixture.nativeElement.textContent;
|
||||
expect(text).toContain('辅助来源 · 接口数据');
|
||||
expect(text).toContain('actual-step');
|
||||
expect(text).toContain('actual-call');
|
||||
expect(text).toContain('来源未完整记录的步骤:2');
|
||||
expect(text).not.toContain('界面来源 · 安卓界面');
|
||||
expect(fixture.nativeElement.querySelector('app-test-result-badge')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not fabricate source metadata for an older backend', () => {
|
||||
const fixture = TestBed.createComponent(EvidenceSourcesComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('details')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
interface SourceReference { step_id?: string; step_number?: number; call_id?: string; source_id?: string; }
|
||||
interface RecordedSource {
|
||||
id: string; kind: string; label: string; target_id: string; plugin_id: string;
|
||||
category: 'interface' | 'auxiliary'; references: SourceReference[]; reference_count: number;
|
||||
}
|
||||
interface SourceSummary {
|
||||
sources: RecordedSource[]; total_sources: number; sources_omitted: number;
|
||||
unattributed_step_count: number; note: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-evidence-sources', standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `@if (value(); as summary) {
|
||||
<details class="sources" aria-label="证据来源与验收边界">
|
||||
<summary>证据来源 · {{ summary.total_sources ? summary.total_sources + ' 个' : '未记录' }}</summary>
|
||||
<p>{{ summary.note }}</p>
|
||||
@for (source of summary.sources; track source.id) {
|
||||
<article>
|
||||
<strong>{{ source.category === 'interface' ? '界面来源' : '辅助来源' }} · {{ kind(source.kind) }}</strong>
|
||||
<div>{{ source.label || source.target_id }}</div>
|
||||
<small>目标 {{ source.target_id }}@if(source.plugin_id) { · 插件 {{ source.plugin_id }} }</small>
|
||||
<ul>
|
||||
@for (ref of source.references; track $index) {
|
||||
<li>步骤 {{ ref.step_number ?? '未记录' }}@if(ref.step_id) { · <code>{{ ref.step_id }}</code> }@if(ref.call_id) { · 调用 <code>{{ ref.call_id }}</code> }</li>
|
||||
}
|
||||
</ul>
|
||||
@if(source.reference_count > source.references.length) { <small>另 {{ source.reference_count - source.references.length }} 条引用见步骤记录。</small> }
|
||||
</article>
|
||||
}
|
||||
@if(summary.sources_omitted) { <p>另 {{ summary.sources_omitted }} 个来源见原始步骤记录。</p> }
|
||||
@if(summary.unattributed_step_count) { <p>来源未完整记录的步骤:{{ summary.unattributed_step_count }};不据此推断执行平台。</p> }
|
||||
</details>
|
||||
}`,
|
||||
styles: [`
|
||||
:host { display:block; margin:10px 0; }
|
||||
.sources { border:1px solid var(--app-border-control,#cbd5e1); border-radius:8px; padding:10px 12px; background:var(--app-surface,#fff); color:var(--app-text-control,#1e293b); font-size:12px; line-height:1.7; overflow-wrap:anywhere; }
|
||||
summary { cursor:pointer; font-weight:600; }
|
||||
article { border-top:1px solid var(--app-border-control,#cbd5e1); margin-top:8px; padding-top:8px; }
|
||||
p,small { color:var(--app-text-secondary,#475569); }
|
||||
ul { padding-left:20px; margin:4px 0; }
|
||||
code { font-size:11px; }
|
||||
`]
|
||||
})
|
||||
export class EvidenceSourcesComponent {
|
||||
value = input<SourceSummary | null>(null);
|
||||
kind(kind: string): string {
|
||||
return ({android:'安卓界面', computer:'电脑界面', api:'接口数据', terminal:'终端', file:'文件', other:'其他'} as Record<string,string>)[kind] || '来源类型未记录';
|
||||
}
|
||||
}
|
||||
+43
-43
@@ -1,78 +1,78 @@
|
||||
:host { display:flex; flex-direction:column; min-height:0; height:100%; overflow:hidden; color:#334155; font-size:13px; }
|
||||
:host { display:flex; flex-direction:column; min-height:0; height:100%; overflow:hidden; color:var(--app-text-control, #334155); font-size:13px; }
|
||||
.exploration-panel { padding:18px; display:flex; flex-direction:column; gap:16px; }
|
||||
header,.row,.actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
header,.row { justify-content:space-between; }
|
||||
header strong { font-size:15px; color:#0f172a; }
|
||||
header strong { font-size:15px; color:var(--app-text-strong, #0f172a); }
|
||||
p { margin:8px 0; line-height:1.65; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||
header p,.muted,small { color:#64748b; font-size:12px; }
|
||||
header p,.muted,small { color:var(--app-text-muted, #64748b); font-size:12px; }
|
||||
button,input,select,textarea { font:inherit; box-sizing:border-box; }
|
||||
button { display:inline-flex; align-items:center; justify-content:center; gap:4px; border:1px solid #dbe3ec; border-radius:8px; background:white; color:#334155; padding:6px 10px; min-height:32px; cursor:pointer; }
|
||||
button:hover:enabled { background:#f1f5f9; border-color:#94a3b8; }
|
||||
button { display:inline-flex; align-items:center; justify-content:center; gap:4px; border:1px solid var(--app-border, #dbe3ec); border-radius:8px; background:var(--app-surface, white); color:var(--app-text-control, #334155); padding:6px 10px; min-height:32px; cursor:pointer; }
|
||||
button:hover:enabled { background:var(--app-surface-soft, #f1f5f9); border-color:var(--app-border-control, #94a3b8); }
|
||||
button:disabled { opacity:.5; cursor:default; }
|
||||
button.primary { background:#0f172a; color:white; border-color:#0f172a; }
|
||||
button.primary { background:#0f172a; color:white; border-color:var(--app-border-control, #0f172a); }
|
||||
.material-symbols-outlined { font-size:18px; }
|
||||
label { display:flex; flex-direction:column; gap:6px; color:#475569; margin:8px 0; }
|
||||
input,select,textarea { width:100%; min-width:0; border:1px solid #cbd5e1; border-radius:8px; padding:8px 10px; background:white; color:#0f172a; }
|
||||
label { display:flex; flex-direction:column; gap:6px; color:var(--app-text-secondary, #475569); margin:8px 0; }
|
||||
input,select,textarea { width:100%; min-width:0; border:1px solid var(--app-border-control, #cbd5e1); border-radius:8px; padding:8px 10px; background:var(--app-surface, white); color:var(--app-text-strong, #0f172a); }
|
||||
textarea { resize:vertical; line-height:1.6; }
|
||||
input:focus,select:focus,textarea:focus { outline:2px solid #94a3b8; outline-offset:1px; }
|
||||
details { border:1px solid #e2e8f0; border-radius:10px; padding:10px 12px; }
|
||||
summary { cursor:pointer; font-size:12px; color:#475569; }
|
||||
.session-summary,.case-editor { padding:12px; border:1px solid #e2e8f0; border-radius:12px; background:#f8fafc; }
|
||||
input:focus,select:focus,textarea:focus { outline:2px solid var(--app-border-control, #94a3b8); outline-offset:1px; }
|
||||
details { border:1px solid var(--app-border, #e2e8f0); border-radius:10px; padding:10px 12px; }
|
||||
summary { cursor:pointer; font-size:12px; color:var(--app-text-secondary, #475569); }
|
||||
.session-summary,.case-editor { padding:12px; border:1px solid var(--app-border, #e2e8f0); border-radius:12px; background:var(--app-surface-soft, #f8fafc); }
|
||||
.session-summary .actions { margin-top:10px; }
|
||||
.conversation { display:flex; flex-direction:column; gap:12px; }
|
||||
.message { padding:10px 12px; border-radius:12px; background:#f1f5f9; border:1px solid transparent; }
|
||||
.message.user { margin-left:20px; background:#ecfdf5; border-color:#d1fae5; }
|
||||
.message.system { padding:4px 8px; background:transparent; color:#64748b; font-size:12px; }
|
||||
.recommendation { border-left:2px solid #94a3b8; padding-left:10px; line-height:1.6; }
|
||||
.message { padding:10px 12px; border-radius:12px; background:var(--app-surface-soft, #f1f5f9); border:1px solid transparent; }
|
||||
.message.user { margin-left:20px; background:var(--app-success-soft, #ecfdf5); border-color:var(--app-success-border, #d1fae5); }
|
||||
.message.system { padding:4px 8px; background:transparent; color:var(--app-text-muted, #64748b); font-size:12px; }
|
||||
.recommendation { border-left:2px solid var(--app-border-control, #94a3b8); padding-left:10px; line-height:1.6; }
|
||||
.recommendation button { margin:6px 0 0 8px; }
|
||||
.composer { border-top:1px solid #e2e8f0; padding-top:4px; }
|
||||
.composer { border-top:1px solid var(--app-border, #e2e8f0); padding-top:4px; }
|
||||
.composer > button { margin-top:8px; }
|
||||
.demo { border:1px solid #bbf7d0; border-radius:12px; padding:12px; background:#f0fdf4; }
|
||||
.demo.warning { background:#fffbeb; border-color:#fcd34d; }
|
||||
.demo { border:1px solid var(--app-success-border, #bbf7d0); border-radius:12px; padding:12px; background:var(--app-success-soft, #f0fdf4); }
|
||||
.demo.warning { background:var(--app-warning-soft, #fffbeb); border-color:#fcd34d; }
|
||||
.record-dot { display:inline-block; width:7px; height:7px; border-radius:50%; background:#94a3b8; margin-right:6px; }
|
||||
.record-dot.recording { background:#dc2626; animation:pulse 1.8s infinite; }
|
||||
@keyframes pulse { 50% { opacity:.4; } }
|
||||
@media(prefers-reduced-motion:reduce) { .record-dot.recording { animation:none; } }
|
||||
.feedback { padding:9px 12px; background:#f0fdf4; border-radius:8px; font-size:12px; }
|
||||
.error { color:#b91c1c; }
|
||||
.feedback.error { background:#fef2f2; }
|
||||
.run,.demo-history { padding:10px 0; border-bottom:1px solid #e2e8f0; }
|
||||
.feedback { padding:9px 12px; background:var(--app-success-soft, #f0fdf4); border-radius:8px; font-size:12px; }
|
||||
.error { color:var(--app-error-text, #b91c1c); }
|
||||
.feedback.error { background:var(--app-error-soft, #fef2f2); }
|
||||
.run,.demo-history { padding:10px 0; border-bottom:1px solid var(--app-border, #e2e8f0); }
|
||||
.run-link { border:none; padding-left:0; background:transparent; text-align:left; }
|
||||
video { width:100%; max-height:260px; margin-top:8px; background:#0f172a; }
|
||||
a { color:#0f766e; font-size:12px; }
|
||||
.empty { color:#64748b; text-align:center; padding:25px 10px; }
|
||||
a { color:var(--app-info-text, #0f766e); font-size:12px; }
|
||||
.empty { color:var(--app-text-muted, #64748b); text-align:center; padding:25px 10px; }
|
||||
.empty .material-symbols-outlined { font-size:32px; }
|
||||
.settings-fields,.case-editor { display:flex; flex-direction:column; gap:3px; }
|
||||
|
||||
app-agent-stream {flex:1;min-height:0;display:block}
|
||||
.composer {flex:none;margin:0 20px 16px;padding:12px 16px;background:#fff;border:1px solid #dbe3ec;border-radius:16px;box-shadow:0 4px 18px #0f172a06;max-height:45%;overflow:auto}
|
||||
.composer {flex:none;margin:0 20px 16px;padding:12px 16px;background:var(--app-surface, #fff);border:1px solid var(--app-border, #dbe3ec);border-radius:16px;box-shadow:0 4px 18px #0f172a06;max-height:45%;overflow:auto}
|
||||
.composer label {margin:0 0 6px}.composer textarea {resize:none}.composer-actions {margin-top:8px}.composer-actions small {flex:1}
|
||||
.session-summary {margin-bottom:18px;background:#fff}.session-summary p {margin:6px 0}.session-summary .row strong {display:flex;align-items:center;gap:6px}
|
||||
.execution-history {margin-top:8px;border:0;padding:4px 0}.case-editor {margin:16px 0;background:#fff}.case-compact {display:flex;flex-wrap:wrap;align-items:center;gap:8px}.case-compact .actions {margin-left:auto}
|
||||
.session-summary {margin-bottom:18px;background:var(--app-surface, #fff)}.session-summary p {margin:6px 0}.session-summary .row strong {display:flex;align-items:center;gap:6px}
|
||||
.execution-history {margin-top:8px;border:0;padding:4px 0}.case-editor {margin:16px 0;background:var(--app-surface, #fff)}.case-compact {display:flex;flex-wrap:wrap;align-items:center;gap:8px}.case-compact .actions {margin-left:auto}
|
||||
.demo {padding:8px 10px;margin-bottom:8px}.demo p {margin:4px 0}
|
||||
|
||||
.composer {margin:0 24px 18px;padding:14px 18px;border-radius:22px;box-shadow:0 6px 24px #0f172a08;max-height:50%;box-sizing:border-box;min-width:0}
|
||||
.composer-heading {display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
|
||||
.composer-heading label {margin:0;font-size:12px;color:#64748b}
|
||||
.composer-heading label {margin:0;font-size:12px;color:var(--app-text-muted, #64748b)}
|
||||
.composer .manual-control {display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin:0 0 10px}
|
||||
.composer .manual-control label {display:flex;flex-direction:row;align-items:center;gap:8px;margin:0;min-width:0}
|
||||
.composer .manual-control select {width:auto;min-width:110px;height:36px;padding:6px 10px}
|
||||
.composer .manual-control button {align-self:center;height:36px;white-space:nowrap}
|
||||
.composer-status {font-size:11px;border-radius:20px;padding:3px 8px;background:#eef2ff;color:#4f46e5}
|
||||
.composer-status.waiting {color:#92400e;background:#fef3c7}
|
||||
.composer-status {font-size:11px;border-radius:20px;padding:3px 8px;background:var(--app-primary-soft, #eef2ff);color:var(--app-exploration-text, #4f46e5)}
|
||||
.composer-status.waiting {color:var(--app-warning-text, #92400e);background:var(--app-warning-soft, #fef3c7)}
|
||||
.composer textarea {border:0;border-radius:0;padding:10px 0;background:transparent;min-height:66px;max-height:140px;font-size:14px;box-shadow:none}
|
||||
.composer textarea:focus {outline:none}
|
||||
.composer:focus-within {border-color:#a5b4fc;box-shadow:0 0 0 3px #eef2ff}
|
||||
.composer-actions {gap:6px;flex-wrap:wrap;border-top:1px solid #f1f5f9;padding-top:10px}
|
||||
.composer-actions .tool-button {border:0;background:#f8fafc;color:#64748b;border-radius:10px;padding:6px 9px}
|
||||
.composer-actions .send-hint {flex:1;text-align:right;font-size:10px;color:#94a3b8;min-width:100px}
|
||||
.composer:focus-within {border-color:var(--app-primary-border, #a5b4fc);box-shadow:0 0 0 3px #eef2ff}
|
||||
.composer-actions {gap:6px;flex-wrap:wrap;border-top:1px solid var(--app-border, #f1f5f9);padding-top:10px}
|
||||
.composer-actions .tool-button {border:0;background:var(--app-surface-soft, #f8fafc);color:var(--app-text-muted, #64748b);border-radius:10px;padding:6px 9px}
|
||||
.composer-actions .send-hint {flex:1;text-align:right;font-size:10px;color:var(--app-text-faint, #94a3b8);min-width:100px}
|
||||
.send-button {min-height:36px;border-radius:12px;padding:7px 12px;background:#4f46e5!important;border-color:#4f46e5!important;gap:6px}
|
||||
.composer-footnote {font-size:11px;color:#94a3b8;margin:8px 0 0}
|
||||
.composer.needs-answer {border-color:#f4c97a}
|
||||
.question-banner {display:flex;gap:10px;background:#fffbeb;border:1px solid #fde68a;border-radius:12px;padding:12px;margin-bottom:12px;color:#92400e}
|
||||
.question-banner > .material-symbols-outlined {font-size:22px;margin-top:2px}.question-banner p {margin:4px 0;font-size:13px}.question-banner strong {font-size:13px}.question-banner button {margin-top:5px;background:#fff7d6;border-color:#f4c97a;color:#92400e}
|
||||
.case-editor {border-color:#c7d2fe;background:linear-gradient(135deg,#f5f7ff,#fff);padding:20px;border-radius:18px}
|
||||
.case-title {display:flex;align-items:center;gap:8px;color:#4f46e5;font-size:14px}.case-title .material-symbols-outlined {font-size:22px}
|
||||
.case-compact {display:block}.case-compact h3 {font-size:18px;line-height:1.5;color:#1e293b;margin:14px 0 6px}.case-compact small {font-size:11px;font-weight:400;background:#e0e7ff;color:#4338ca;padding:3px 6px;border-radius:5px;white-space:nowrap}.case-compact .actions {margin:16px 0 0}.case-compact .primary {background:#4f46e5;border-color:#4f46e5}
|
||||
.session-summary {padding:12px 16px;border-radius:14px}.exploration-goal{display:flex;align-items:flex-start;gap:6px}.session-summary .exploration-goal p{flex:1;min-width:0;margin:4px 0}.case-compact h3{display:flex;align-items:flex-start;gap:6px}.case-compact h3>span{min-width:0;overflow-wrap:anywhere}.session-summary p {color:#64748b;font-size:12px}
|
||||
.composer-footnote {font-size:11px;color:var(--app-text-faint, #94a3b8);margin:8px 0 0}
|
||||
.composer.needs-answer {border-color:var(--app-warning-border, #f4c97a)}
|
||||
.question-banner {display:flex;gap:10px;background:var(--app-warning-soft, #fffbeb);border:1px solid var(--app-warning-border, #fde68a);border-radius:12px;padding:12px;margin-bottom:12px;color:var(--app-warning-text, #92400e)}
|
||||
.question-banner > .material-symbols-outlined {font-size:22px;margin-top:2px}.question-banner p {margin:4px 0;font-size:13px}.question-banner strong {font-size:13px}.question-banner button {margin-top:5px;background:var(--app-warning-soft, #fff7d6);border-color:var(--app-warning-border, #f4c97a);color:var(--app-warning-text, #92400e)}
|
||||
.case-editor {border-color:var(--app-primary-border, #c7d2fe);background:linear-gradient(135deg,var(--app-primary-soft, #f5f7ff),var(--app-surface, #fff));padding:20px;border-radius:18px}
|
||||
.case-title {display:flex;align-items:center;gap:8px;color:var(--app-exploration-text, #4f46e5);font-size:14px}.case-title .material-symbols-outlined {font-size:22px}
|
||||
.case-compact {display:block}.case-compact h3 {font-size:18px;line-height:1.5;color:var(--app-text-control, #1e293b);margin:14px 0 6px}.case-compact small {font-size:11px;font-weight:400;background:var(--app-primary-soft, #e0e7ff);color:var(--app-exploration-text, #4338ca);padding:3px 6px;border-radius:5px;white-space:nowrap}.case-compact .actions {margin:16px 0 0}.case-compact .primary {background:#4f46e5;border-color:#4f46e5}
|
||||
.session-summary {padding:12px 16px;border-radius:14px}.exploration-goal{display:flex;align-items:flex-start;gap:6px}.session-summary .exploration-goal p{flex:1;min-width:0;margin:4px 0}.case-compact h3{display:flex;align-items:flex-start;gap:6px}.case-compact h3>span{min-width:0;overflow-wrap:anywhere}.session-summary p {color:var(--app-text-muted, #64748b);font-size:12px}
|
||||
@media(max-width:600px) {.composer {margin:0 12px 12px;padding:12px}.send-hint {display:none}.send-button {margin-left:auto}.question-banner {max-height:160px;overflow:auto}.case-editor {padding:14px}}
|
||||
@@ -8,7 +8,7 @@ import { FavoriteKind, FavoritesService } from '../../services/favorites.service
|
||||
(click)="toggle($event)"><span class="material-symbols-outlined" aria-hidden="true">{{favorite() ? 'star' : 'star_outline'}}</span></button>
|
||||
@if(error()){<span class="feedback" role="alert">{{error()}}</span>}
|
||||
@if(api.error() && !api.loaded()){<button type="button" class="retry" (click)="$event.stopPropagation(); api.load(true)" title="重试加载收藏">重试</button>}`,
|
||||
styles: [`:host{display:inline-flex;align-items:center;position:relative;flex-shrink:0}button{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:7px;background:transparent;color:#64748b;cursor:pointer}button:hover{background:#fef3c7;color:#b45309}button.saved{color:#c47b0b}button.saved span{font-variation-settings:'FILL' 1}button:focus-visible{outline:2px solid #818cf8;outline-offset:2px}button:disabled{opacity:.45;cursor:default}.material-symbols-outlined{font-size:19px}.feedback{position:absolute;left:0;top:30px;min-width:170px;max-width:260px;padding:8px 10px;z-index:20;border:1px solid #fecaca;border-radius:8px;background:#fff7f7;color:#b91c1c;font-size:12px;line-height:1.5;box-shadow:0 4px 16px #0f172a12}.retry{font-size:10px;width:auto}`]
|
||||
styles: [`:host{display:inline-flex;align-items:center;position:relative;flex-shrink:0}button{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:7px;background:transparent;color:var(--app-text-muted, #64748b);cursor:pointer}button:hover{background:var(--app-warning-soft, #fef3c7);color:var(--app-warning-text, #b45309)}button.saved{color:var(--app-warning-text, #c47b0b)}button.saved span{font-variation-settings:'FILL' 1}button:focus-visible{outline:2px solid var(--app-exploration-border, #818cf8);outline-offset:2px}button:disabled{opacity:.45;cursor:default}.material-symbols-outlined{font-size:19px}.feedback{position:absolute;left:0;top:30px;min-width:170px;max-width:260px;padding:8px 10px;z-index:20;border:1px solid var(--app-error-border, #fecaca);border-radius:8px;background:var(--app-error-soft, #fff7f7);color:var(--app-error-text, #b91c1c);font-size:12px;line-height:1.5;box-shadow:0 4px 16px #0f172a12}.retry{font-size:10px;width:auto}`]
|
||||
})
|
||||
export class FavoriteButtonComponent {
|
||||
api = inject(FavoritesService);
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
:host { display: block; width: 100%; color: #334155; }
|
||||
:host { display: block; width: 100%; color: var(--app-text-control, #334155); }
|
||||
.featured { margin-top: 26px; }
|
||||
.heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
h2 { display: flex; align-items: center; gap: 7px; margin: 0; font-size: 16px; font-weight: 650; color: #1e293b; }
|
||||
h2 span { font-size: 20px; color: #d29a32; }
|
||||
.heading p { font-size: 12px; color: #64748b; margin: 6px 0 0; }
|
||||
h2 { display: flex; align-items: center; gap: 7px; margin: 0; font-size: 16px; font-weight: 650; color: var(--app-text-control, #1e293b); }
|
||||
h2 span { font-size: 20px; color: var(--app-warning-text, #d29a32); }
|
||||
.heading p { font-size: 12px; color: var(--app-text-muted, #64748b); margin: 6px 0 0; }
|
||||
.heading > input { max-width: 210px; width: 40%; border-radius: 20px; }
|
||||
.filters { display: flex; gap: 5px; flex-wrap: wrap; margin: 17px 0; }
|
||||
.filters button { border: 0; padding: 6px 13px; border-radius: 18px; font-size: 12px; background: transparent; color: #64748b; }
|
||||
.filters button.selected { background: #e0e7ff; color: #4338ca; font-weight: 600; }
|
||||
.filters button { border: 0; padding: 6px 13px; border-radius: 18px; font-size: 12px; background: transparent; color: var(--app-text-muted, #64748b); }
|
||||
.filters button.selected { background: var(--app-primary-soft, #e0e7ff); color: var(--app-exploration-text, #4338ca); font-weight: 600; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 14px; align-items: start; }
|
||||
.card { padding: 16px; border: 1px solid #e2e8f0; border-radius: 16px; background: #fff; min-width: 0; transition: border-color .15s, box-shadow .15s; }
|
||||
.card:hover { border-color: #c7d2fe; box-shadow: 0 4px 16px #33415508; }
|
||||
.card.expanded { border-color: #a5b4fc; }
|
||||
.card { padding: 16px; border: 1px solid var(--app-border, #e2e8f0); border-radius: 16px; background: var(--app-surface, #fff); min-width: 0; transition: border-color .15s, box-shadow .15s; }
|
||||
.card:hover { border-color: var(--app-primary-border, #c7d2fe); box-shadow: 0 4px 16px #33415508; }
|
||||
.card.expanded { border-color: var(--app-primary-border, #a5b4fc); }
|
||||
.card-overview { display: flex; flex-direction: column; min-height: 178px; }
|
||||
.card-heading { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-height: 22px; }
|
||||
.mode { padding: 3px 8px; background: #f1f5f9; color: #475569; border-radius: 6px; font-size: 11px; line-height: 16px; }
|
||||
.mode.pro { background: #ede9fe; color: #6d28d9; }
|
||||
.mode.explore { background: #e0f2fe; color: #0369a1; }
|
||||
.case-version { font-size: 11px; color: #94a3b8; white-space: nowrap; }
|
||||
.mode { padding: 3px 8px; background: var(--app-surface-soft, #f1f5f9); color: var(--app-text-secondary, #475569); border-radius: 6px; font-size: 11px; line-height: 16px; }
|
||||
.mode.pro { background: var(--app-exploration-soft, #ede9fe); color: var(--app-exploration-text, #6d28d9); }
|
||||
.mode.explore { background: var(--app-info-soft, #e0f2fe); color: var(--app-info-text, #0369a1); }
|
||||
.case-version { font-size: 11px; color: var(--app-text-faint, #94a3b8); white-space: nowrap; }
|
||||
.card-title { display: flex; align-items: flex-start; gap: 6px; margin: 13px 0 7px; min-height: 42px; }
|
||||
.card-title h3 { flex: 1; min-width: 0; margin: 0; font-size: 14px; line-height: 21px; overflow-wrap: anywhere; color: #1e293b;
|
||||
.card-title h3 { flex: 1; min-width: 0; margin: 0; font-size: 14px; line-height: 21px; overflow-wrap: anywhere; color: var(--app-text-control, #1e293b);
|
||||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
|
||||
.star { display: flex; flex-shrink: 0; width: 24px; height: 24px; align-items: center; justify-content: center; padding: 0; border: 0; background: transparent; color: #c58b23; }
|
||||
.star { display: flex; flex-shrink: 0; width: 24px; height: 24px; align-items: center; justify-content: center; padding: 0; border: 0; background: transparent; color: var(--app-warning-text, #c58b23); }
|
||||
.star span { font-size: 19px; font-variation-settings: 'FILL' 1; }
|
||||
.model-summary { font-size: 12px; line-height: 18px; color: #64748b; margin: 0 0 12px; min-height: 36px; overflow-wrap: anywhere;
|
||||
.model-summary { font-size: 12px; line-height: 18px; color: var(--app-text-muted, #64748b); margin: 0 0 12px; min-height: 36px; overflow-wrap: anywhere;
|
||||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
|
||||
.card-actions { display: flex; align-items: center; justify-content: space-between; margin-top: auto; gap: 10px; }
|
||||
.card-actions button { display: flex; align-items: center; gap: 4px; min-height: 34px; padding: 7px 10px; border-radius: 8px; font-size: 12px; white-space: nowrap; }
|
||||
.card-actions span { font-size: 16px; }
|
||||
.configure { color: #64748b; border: 0; background: transparent; }
|
||||
.configure { color: var(--app-text-muted, #64748b); border: 0; background: transparent; }
|
||||
.primary { border: 1px solid #6366f1; background: #6366f1; color: white; }
|
||||
.primary:hover { background: #4f46e5; }
|
||||
.configuration { border-top: 1px solid #e2e8f0; margin-top: 14px; padding-top: 14px; display: grid; gap: 12px; }
|
||||
.configuration label { display: grid; gap: 6px; font-size: 12px; color: #475569; }
|
||||
.configuration { border-top: 1px solid var(--app-border, #e2e8f0); margin-top: 14px; padding-top: 14px; display: grid; gap: 12px; }
|
||||
.configuration label { display: grid; gap: 6px; font-size: 12px; color: var(--app-text-secondary, #475569); }
|
||||
.configuration .checkbox { display: flex; align-items: center; }
|
||||
.checkbox input { width: auto; min-height: auto; }
|
||||
input, select { box-sizing: border-box; min-width: 0; width: 100%; min-height: 40px; padding: 10px 12px; border: 1px solid #dbe3ec; border-radius: 10px; color: #334155; background: white; font: inherit; font-size: 13px; }
|
||||
option { color: #334155; background: #fff; }
|
||||
.hint { font-size: 11px; color: #64748b; line-height: 1.5; margin: 7px 0 0; }
|
||||
.save { justify-self: start; padding: 7px 10px; border: 1px solid #cbd5e1; background: #fff; color: #475569; border-radius: 8px; font-size: 12px; }
|
||||
details { border: 1px solid #e2e8f0; padding: 10px; border-radius: 9px; }
|
||||
input, select { box-sizing: border-box; min-width: 0; width: 100%; min-height: 40px; padding: 10px 12px; border: 1px solid var(--app-border, #dbe3ec); border-radius: 10px; color: var(--app-text-control, #334155); background: var(--app-surface, white); font: inherit; font-size: 13px; }
|
||||
option { color: var(--app-text-control, #334155); background: var(--app-surface, #fff); }
|
||||
.hint { font-size: 11px; color: var(--app-text-muted, #64748b); line-height: 1.5; margin: 7px 0 0; }
|
||||
.save { justify-self: start; padding: 7px 10px; border: 1px solid var(--app-border-control, #cbd5e1); background: var(--app-surface, #fff); color: var(--app-text-secondary, #475569); border-radius: 8px; font-size: 12px; }
|
||||
details { border: 1px solid var(--app-border, #e2e8f0); padding: 10px; border-radius: 9px; }
|
||||
details summary { font-size: 12px; cursor: pointer; overflow-wrap: anywhere; }
|
||||
details[open] > label, details[open] > app-setting-slider { margin-top: 12px; display: block; }
|
||||
details[open] > app-model-selector { margin-top: 12px; }
|
||||
button { font-family: inherit; cursor: pointer; }
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
|
||||
.empty { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 28px 18px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; font-size: 12px; line-height: 1.7; }
|
||||
.empty > span { font-size: 28px; color: #c5a365; margin-bottom: 9px; }
|
||||
.empty strong { font-size: 14px; color: #475569; }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible { outline: 2px solid var(--app-exploration-border, #818cf8); outline-offset: 2px; }
|
||||
.empty { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 28px 18px; border: 1px dashed var(--app-border-control, #cbd5e1); border-radius: 16px; color: var(--app-text-muted, #64748b); font-size: 12px; line-height: 1.7; }
|
||||
.empty > span { font-size: 28px; color: var(--app-warning-text, #c5a365); margin-bottom: 9px; }
|
||||
.empty strong { font-size: 14px; color: var(--app-text-secondary, #475569); }
|
||||
.empty p { max-width: 440px; margin: 8px 0 0; }
|
||||
.error, .notice { padding: 10px 12px; border-radius: 8px; font-size: 12px; line-height: 1.6; }
|
||||
.error { background: #fff1f2; color: #b91c1c; }
|
||||
.notice { background: #f0fdf4; color: #166534; }
|
||||
.error { background: var(--app-error-soft, #fff1f2); color: var(--app-error-text, #b91c1c); }
|
||||
.notice { background: var(--app-success-soft, #f0fdf4); color: var(--app-success-text, #166534); }
|
||||
@media (max-width: 560px) {
|
||||
.heading { align-items: flex-start; flex-direction: column; }
|
||||
.heading > input { width: 100%; max-width: none; }
|
||||
|
||||
+111
-111
@@ -32,12 +32,12 @@
|
||||
width: 400px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: calc(100dvh - var(--player-top, 60px) - 12px);
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.84);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 255 255 255) / 0.85);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 50px -10px rgba(15, 23, 42, 0.22), 0 0 0 1px rgba(226, 232, 240, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 20px 50px -10px rgba(15, 23, 42, 0.22), 0 0 0 1px rgba(226, 232, 240, 0.6), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.9);
|
||||
z-index: 1050;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -80,9 +80,9 @@
|
||||
/* --- Window Header (Draggable Handle) --- */
|
||||
.window-header {
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.75);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid rgba(226, 232, 240, 0.75);
|
||||
border-bottom: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -104,14 +104,14 @@
|
||||
|
||||
.header-icon {
|
||||
font-size: 18px;
|
||||
color: #2563eb;
|
||||
color: var(--app-primary-text, #2563eb);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -121,9 +121,9 @@
|
||||
.mode-switcher-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(241, 245, 249, 0.65);
|
||||
background: rgb(var(--app-surface-soft-rgb, 241 245 249) / 0.65);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.8);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
margin-left: 4px;
|
||||
@@ -132,7 +132,7 @@
|
||||
.mode-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 7px;
|
||||
@@ -152,8 +152,8 @@
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #0f172a;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.6);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -180,9 +180,9 @@
|
||||
.mode-toggle-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(241, 245, 249, 0.7);
|
||||
background: rgb(var(--app-surface-soft-rgb, 241 245 249) / 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.85);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
margin-right: 4px;
|
||||
@@ -194,7 +194,7 @@
|
||||
gap: 3px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
@@ -207,8 +207,8 @@
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #0f172a;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.8);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -222,9 +222,9 @@
|
||||
.speed-pill-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(241, 245, 249, 0.65);
|
||||
background: rgb(var(--app-surface-soft-rgb, 241 245 249) / 0.65);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.8);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
margin-right: 4px;
|
||||
@@ -232,7 +232,7 @@
|
||||
.speed-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
color: var(--app-text-control, #334155);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
@@ -241,8 +241,8 @@
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #0f172a;
|
||||
background: #cbd5e1;
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
background: var(--app-surface-raised, #cbd5e1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -255,11 +255,11 @@
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
background: rgba(241, 245, 249, 0.75);
|
||||
background: rgb(var(--app-surface-soft-rgb, 241 245 249) / 0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
color: #1e293b;
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.85);
|
||||
color: var(--app-text-control, #1e293b);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -267,21 +267,21 @@
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.8);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #0f172a;
|
||||
border-color: rgba(203, 213, 225, 0.95);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.95);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
border-color: rgb(var(--app-border-control-rgb, 203 213 225) / 0.95);
|
||||
transform: translateY(-0.5px);
|
||||
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
&.close-btn:hover {
|
||||
background: rgba(254, 226, 226, 0.95);
|
||||
color: #dc2626;
|
||||
border-color: rgba(252, 165, 165, 0.9);
|
||||
background: rgb(var(--app-error-soft-rgb, 254 226 226) / 0.95);
|
||||
color: var(--app-error-text, #dc2626);
|
||||
border-color: rgb(var(--app-error-border-rgb, 252 165 165) / 0.9);
|
||||
box-shadow: 0 2px 6px rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
.video-body {
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
@@ -314,16 +314,16 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
.spinning-icon {
|
||||
font-size: 28px;
|
||||
color: #2563eb;
|
||||
color: var(--app-primary-text, #2563eb);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
min-height: 260px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
}
|
||||
|
||||
.live-status-overlay {
|
||||
@@ -363,9 +363,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fca5a5;
|
||||
color: #dc2626;
|
||||
background: var(--app-error-soft, #fef2f2);
|
||||
border: 1px solid var(--app-error-border, #fca5a5);
|
||||
color: var(--app-error-text, #dc2626);
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
padding: 3px 9px;
|
||||
@@ -384,10 +384,10 @@
|
||||
|
||||
.live-refresh-btn {
|
||||
pointer-events: auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.95);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid #cbd5e1;
|
||||
color: #1e293b;
|
||||
border: 1px solid var(--app-border-control, #cbd5e1);
|
||||
color: var(--app-text-control, #1e293b);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -399,9 +399,9 @@
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
border-color: #94a3b8;
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
border-color: var(--app-border-control, #94a3b8);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@
|
||||
.step-replay-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -424,16 +424,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
background: var(--app-warning-soft, #fffbeb);
|
||||
color: var(--app-warning-text, #b45309);
|
||||
padding: 6px 12px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #fde68a;
|
||||
border-bottom: 1px solid var(--app-warning-border, #fde68a);
|
||||
|
||||
.notice-icon {
|
||||
font-size: 14px;
|
||||
color: #d97706;
|
||||
color: var(--app-warning-text, #d97706);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@@ -443,7 +443,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: #f8fafc;
|
||||
background: var(--app-surface-soft, #f8fafc);
|
||||
min-height: 280px;
|
||||
max-height: 520px;
|
||||
overflow: hidden;
|
||||
@@ -458,21 +458,21 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
background: var(--app-surface-soft, #f8fafc);
|
||||
color: var(--app-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
z-index: 5;
|
||||
|
||||
.spinning-icon {
|
||||
font-size: 24px;
|
||||
color: #0284c7;
|
||||
color: var(--app-info-text, #0284c7);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.broken-icon {
|
||||
font-size: 28px;
|
||||
color: #94a3b8;
|
||||
color: var(--app-text-faint, #94a3b8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px -2px rgba(15, 23, 42, 0.14), 0 0 0 1px rgba(226, 232, 240, 0.9);
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
@@ -532,7 +532,7 @@
|
||||
background: rgba(15, 23, 42, 0.88);
|
||||
backdrop-filter: blur(14px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 255 255 255) / 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 6px 10px;
|
||||
color: #ffffff;
|
||||
@@ -580,7 +580,7 @@
|
||||
|
||||
.action-type-icon {
|
||||
font-size: 15px;
|
||||
color: #38bdf8;
|
||||
color: var(--app-info-text, #38bdf8);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@@ -595,8 +595,8 @@
|
||||
.pre-post-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.12);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 255 255 255) / 0.15);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
@@ -614,7 +614,7 @@
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -639,11 +639,11 @@
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.15);
|
||||
}
|
||||
|
||||
&.pinned {
|
||||
color: #38bdf8;
|
||||
color: var(--app-info-text, #38bdf8);
|
||||
background: rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
|
||||
@@ -664,7 +664,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
|
||||
video {
|
||||
min-height: 0;
|
||||
@@ -674,21 +674,21 @@
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
}
|
||||
}
|
||||
|
||||
.video-controls-footer {
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.84);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border-top: 1px solid rgba(226, 232, 240, 0.75);
|
||||
border-top: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.75);
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.9);
|
||||
|
||||
.scrubber-bar-container {
|
||||
position: relative;
|
||||
@@ -714,7 +714,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 5px;
|
||||
background: rgba(226, 232, 240, 0.85);
|
||||
background: rgb(var(--app-surface-raised-rgb, 226 232 240) / 0.85);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
padding: 0;
|
||||
border: 2px solid #fff;
|
||||
border: 2px solid var(--app-border, #fff);
|
||||
border-radius: 50%;
|
||||
background: #64748b;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.25);
|
||||
@@ -775,8 +775,8 @@
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1.5px solid #94a3b8;
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.9);
|
||||
border: 1.5px solid var(--app-border-control, #94a3b8);
|
||||
transform: translate(-50%, 0);
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
@@ -785,17 +785,17 @@
|
||||
&:hover {
|
||||
transform: translate(-50%, 0) scale(1.4);
|
||||
border-color: #0284c7;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
}
|
||||
|
||||
&.passed {
|
||||
background: #0284c7;
|
||||
border-color: #ffffff;
|
||||
border-color: var(--app-border, #ffffff);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #38bdf8;
|
||||
border-color: #ffffff;
|
||||
border-color: var(--app-border, #ffffff);
|
||||
transform: translate(-50%, 0) scale(1.3);
|
||||
box-shadow: 0 0 6px rgba(56, 189, 248, 0.8);
|
||||
}
|
||||
@@ -815,12 +815,12 @@
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
background: rgba(241, 245, 249, 0.75);
|
||||
background: rgb(var(--app-surface-soft-rgb, 241 245 249) / 0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.85);
|
||||
color: #0f172a;
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.85);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.85);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
@@ -831,9 +831,9 @@
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: rgba(191, 219, 254, 0.9);
|
||||
color: #0284c7;
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.95);
|
||||
border-color: rgb(var(--app-primary-border-rgb, 191 219 254) / 0.9);
|
||||
color: var(--app-info-text, #0284c7);
|
||||
transform: translateY(-0.5px);
|
||||
box-shadow: 0 3px 8px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
@@ -843,8 +843,8 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #0284c7;
|
||||
background: rgba(224, 242, 254, 0.85);
|
||||
color: var(--app-info-text, #0284c7);
|
||||
background: rgb(var(--app-info-soft-rgb, 224 242 254) / 0.85);
|
||||
border-color: rgba(56, 189, 248, 0.8);
|
||||
box-shadow: 0 2px 6px rgba(2, 132, 199, 0.15);
|
||||
}
|
||||
@@ -853,16 +853,16 @@
|
||||
background: linear-gradient(135deg, rgba(2, 132, 199, 0.92), rgba(37, 99, 235, 0.88));
|
||||
backdrop-filter: blur(14px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 255 255 255) / 0.35);
|
||||
color: #ffffff;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 14px rgba(2, 132, 199, 0.35), inset 0 1px 1px rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 4px 14px rgba(2, 132, 199, 0.35), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.5);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, rgba(2, 132, 199, 1), rgba(37, 99, 235, 0.95));
|
||||
box-shadow: 0 6px 18px rgba(2, 132, 199, 0.45), inset 0 1px 1px rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 6px 18px rgba(2, 132, 199, 0.45), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.6);
|
||||
transform: translateY(-1px) scale(1.04);
|
||||
}
|
||||
|
||||
@@ -883,18 +883,18 @@
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
|
||||
color: #334155;
|
||||
color: var(--app-text-control, #334155);
|
||||
letter-spacing: -0.2px;
|
||||
|
||||
&:hover {
|
||||
color: #0284c7;
|
||||
border-color: rgba(191, 219, 254, 0.9);
|
||||
color: var(--app-info-text, #0284c7);
|
||||
border-color: rgb(var(--app-primary-border-rgb, 191 219 254) / 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
color: #1e293b;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,7 +902,7 @@
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
|
||||
color: #1e293b;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
margin-left: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -911,7 +911,7 @@
|
||||
|
||||
/* --- Clean, Minimal High-Contrast Empty State --- */
|
||||
.empty-video-fallback {
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
padding: 36px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -924,8 +924,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fee2e2;
|
||||
background: var(--app-error-soft, #fef2f2);
|
||||
border: 1px solid var(--app-error-border, #fee2e2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -933,7 +933,7 @@
|
||||
|
||||
.fallback-icon {
|
||||
font-size: 24px;
|
||||
color: #ef4444;
|
||||
color: var(--app-error-text, #ef4444);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,13 +941,13 @@
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
}
|
||||
|
||||
.fallback-desc {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
max-width: 240px;
|
||||
}
|
||||
@@ -1026,25 +1026,25 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: var(--app-surface-soft, #f8fafc);
|
||||
border-bottom: 1px solid var(--app-border, #e2e8f0);
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
|
||||
select {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
border: 1px solid #cbd5e1;
|
||||
border: 1px solid var(--app-border-control, #cbd5e1);
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
background: white;
|
||||
background: var(--app-surface, white);
|
||||
}
|
||||
}
|
||||
|
||||
.recording-notice {
|
||||
padding: 8px 12px;
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
background: var(--app-warning-soft, #fffbeb);
|
||||
color: var(--app-warning-text, #92400e);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1055,8 +1055,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -1066,13 +1066,13 @@
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 12px;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.recording-skip-notice {
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
+46
-46
@@ -15,10 +15,10 @@
|
||||
*/
|
||||
|
||||
$border-color: rgba(0, 0, 0, 0.08);
|
||||
$text-primary: #1f2937;
|
||||
$text-secondary: #4b5563;
|
||||
$text-muted: #9ca3af;
|
||||
$bg-main: #f9fafb;
|
||||
$text-primary: var(--app-text-strong, #1f2937);
|
||||
$text-secondary: var(--app-text-secondary, #4b5563);
|
||||
$text-muted: var(--app-text-faint, #9ca3af);
|
||||
$bg-main: var(--app-surface-soft, #f9fafb);
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -32,7 +32,7 @@ $bg-main: #f9fafb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.4);
|
||||
backdrop-filter: blur(16px);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -42,7 +42,7 @@ $bg-main: #f9fafb;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.6);
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
@@ -74,10 +74,10 @@ $bg-main: #f9fafb;
|
||||
|
||||
.session-dropdown {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.12);
|
||||
border-radius: 8px;
|
||||
font-size: 13.5px;
|
||||
background-color: #ffffff;
|
||||
background-color: var(--app-surface, #ffffff);
|
||||
outline: none;
|
||||
color: $text-primary;
|
||||
min-width: 250px;
|
||||
@@ -92,8 +92,8 @@ $bg-main: #f9fafb;
|
||||
|
||||
.btn-refresh {
|
||||
padding: 8px 14px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: var(--app-surface, #ffffff);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.12);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -105,7 +105,7 @@ $bg-main: #f9fafb;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
background: var(--app-surface-raised, #f3f4f6);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -119,7 +119,7 @@ $bg-main: #f9fafb;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
background: rgba(249, 250, 251, 0.3);
|
||||
background: rgb(var(--app-surface-soft-rgb, 249 250 251) / 0.3);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -172,8 +172,8 @@ $bg-main: #f9fafb;
|
||||
gap: 16px;
|
||||
|
||||
.log-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: var(--app-surface, #ffffff);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.01), 0 2px 4px -1px rgba(0, 0, 0, 0.006);
|
||||
@@ -214,24 +214,24 @@ $bg-main: #f9fafb;
|
||||
font-family: monospace;
|
||||
|
||||
&.llm_stream {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
background: var(--app-info-soft, #e0f2fe);
|
||||
color: var(--app-info-text, #0369a1);
|
||||
}
|
||||
&.trace_recorded {
|
||||
background: #f3e8ff;
|
||||
color: #6b21a8;
|
||||
background: var(--app-exploration-soft, #f3e8ff);
|
||||
color: var(--app-exploration-text, #6b21a8);
|
||||
}
|
||||
&.step_recorded {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
background: var(--app-success-soft, #ecfdf5);
|
||||
color: var(--app-success-text, #047857);
|
||||
}
|
||||
&.step_updated {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
background: var(--app-warning-soft, #fef3c7);
|
||||
color: var(--app-warning-text, #b45309);
|
||||
}
|
||||
&.background_tasks_updated {
|
||||
background: #fff1f2;
|
||||
color: #be123c;
|
||||
background: var(--app-error-soft, #fff1f2);
|
||||
color: var(--app-error-text, #be123c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,28 +260,28 @@ $bg-main: #f9fafb;
|
||||
}
|
||||
|
||||
.summary-bubble {
|
||||
background: #f1f5f9;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-left: 4px solid #475569;
|
||||
background: var(--app-surface-soft, #f1f5f9);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.05);
|
||||
border-left: 4px solid var(--app-border-control, #475569);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
}
|
||||
}
|
||||
|
||||
details {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.06);
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
background: var(--app-surface-soft, #fafafa);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&[open] {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
border-color: rgb(var(--app-border-control-rgb, 0 0 0) / 0.12);
|
||||
.details-summary {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.06);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ $bg-main: #f9fafb;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
@@ -306,18 +306,18 @@ $bg-main: #f9fafb;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
color: var(--app-text-control, #334155);
|
||||
|
||||
&.thinking-text {
|
||||
background: #f0fdf4;
|
||||
color: #14532d;
|
||||
background: var(--app-success-soft, #f0fdf4);
|
||||
color: var(--app-success-text, #14532d);
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
&.planning-text {
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
background: var(--app-surface-soft, #f8fafc);
|
||||
color: var(--app-text-control, #334155);
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
@@ -326,28 +326,28 @@ $bg-main: #f9fafb;
|
||||
&.thinking-details {
|
||||
border-left: 4px solid #16a34a;
|
||||
.details-summary {
|
||||
color: #16a34a;
|
||||
color: var(--app-success-text, #16a34a);
|
||||
}
|
||||
}
|
||||
|
||||
&.planning-details {
|
||||
border-left: 4px solid #4f46e5;
|
||||
.details-summary {
|
||||
color: #4f46e5;
|
||||
color: var(--app-exploration-text, #4f46e5);
|
||||
}
|
||||
}
|
||||
|
||||
&.tool-details {
|
||||
border-left: 4px solid #2563eb;
|
||||
.details-summary {
|
||||
color: #2563eb;
|
||||
color: var(--app-primary-text, #2563eb);
|
||||
}
|
||||
}
|
||||
|
||||
&.result-details {
|
||||
border-left: 4px solid #0891b2;
|
||||
.details-summary {
|
||||
color: #0891b2;
|
||||
color: var(--app-info-text, #0891b2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,7 +355,7 @@ $bg-main: #f9fafb;
|
||||
|
||||
&.raw-body {
|
||||
details {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgb(var(--app-border-control-rgb, 0 0 0) / 0.06);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -366,7 +366,7 @@ $bg-main: #f9fafb;
|
||||
color: $text-secondary;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface, #ffffff);
|
||||
}
|
||||
|
||||
.details-content {
|
||||
@@ -392,11 +392,11 @@ $bg-main: #f9fafb;
|
||||
height: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -47,7 +47,7 @@
|
||||
@if (review.last_failure_category) { <dt>最近错误类别</dt><dd>{{ review.last_failure_category }}</dd> }
|
||||
@if (review.status === 'budget_exhausted') { <dt>结束原因</dt><dd class="error">取证、模型请求与重试已耗尽验收总预算;未确认通过。</dd> }
|
||||
@if (review.status === 'request_retries_exhausted') { <dt>结束原因</dt><dd class="error">验收模型请求重试已用尽;未确认通过,没有重跑设备操作。</dd> }
|
||||
@if (review.status === 'cancelled') { <dt>结束原因</dt><dd>验收被停止或指导中断,不作为请求超时重试。</dd> }
|
||||
@if (review.status === 'cancelled') { <dt>结束原因</dt><dd>{{ cancellationReason(review.cancel_reason) }}</dd> }
|
||||
</dl>
|
||||
</details>
|
||||
}
|
||||
@@ -57,7 +57,12 @@
|
||||
<dl>
|
||||
<dt>本地请求编号</dt><dd>{{ request.request_id }}</dd>
|
||||
<dt>本次尝试编号</dt><dd>{{ request.attempt_id }}</dd>
|
||||
@if (request.previous_attempt_id) { <dt>上次尝试编号</dt><dd>{{ request.previous_attempt_id }}</dd> }
|
||||
<dt>出站请求编号</dt><dd>{{ request.client_request_id || '未记录' }}</dd>
|
||||
<dt>服务端请求编号</dt><dd>{{ request.server_request_id || '服务未提供' }}</dd>
|
||||
<dt>响应编号</dt><dd>{{ request.response_id || '服务未提供' }}</dd>
|
||||
@if (request.server_trace_id) { <dt>服务端链路编号</dt><dd>{{ request.server_trace_id }}</dd> }
|
||||
@if (request.traceparent) { <dt>链路上下文</dt><dd>{{ request.traceparent }}</dd> }
|
||||
<dt>模型配置</dt><dd>{{ request.model || '未记录' }} / {{ request.reasoning_effort || '默认' }} · 第 {{ request.attempt }} 次请求,最多重试 {{ request.max_retries }} 次</dd>
|
||||
@if (request.final_review; as review) { <dt>所属最终验收</dt><dd>{{ review.review_id }} · {{ reviewStage(review) }} · 请求记录时剩余 {{ review.remaining_seconds }} 秒</dd> }
|
||||
<dt>输出 Token 上限</dt><dd>{{ request.max_output_tokens || '未设置(由模型服务决定)' }}</dd>
|
||||
@@ -68,6 +73,8 @@
|
||||
<dt>首个有效输出</dt><dd>{{ request.first_output_at ? (request.first_output_at * 1000 | date:'HH:mm:ss.SSS') : '未收到/未记录' }}</dd>
|
||||
<dt>最后有效输出</dt><dd>{{ request.last_output_at ? (request.last_output_at * 1000 | date:'HH:mm:ss.SSS') : '未收到/未记录' }}</dd>
|
||||
<dt>结束时间</dt><dd>{{ request.ended_at ? (request.ended_at * 1000 | date:'HH:mm:ss.SSS') : '尚未记录' }}</dd>
|
||||
@if (termination(request); as reason) { <dt>结束原因</dt><dd>{{ reason }}</dd> }
|
||||
@if (request.cancel_phase) { <dt>中断阶段</dt><dd>{{ phase(request.cancel_phase) }}</dd> }
|
||||
<dt>输入图片</dt><dd>{{ request.images.length }} 张 @for (image of request.images; track $index) { <span class="image-size">{{ image.width && image.height ? image.width + ' × ' + image.height : '尺寸未知' }}</span> }</dd>
|
||||
<dt>Token 用量</dt><dd>{{ request.usage ? '输入 ' + request.usage.prompt_tokens + ' / 输出 ' + request.usage.completion_tokens + ' / 合计 ' + request.usage.total_tokens : '未知(服务未报告)' }}</dd>
|
||||
@if (request.error) { <dt>失败原因</dt><dd class="error">{{ request.error_type }} · {{ request.error }} · 阶段 {{ phase(request.failure_phase || '') }} @if (request.category) { · {{ request.category }} }</dd> }
|
||||
|
||||
+15
-15
@@ -1,26 +1,26 @@
|
||||
:host { display: block; }
|
||||
.model-requests { margin: 10px 0 16px; color: #475569; font-size: 12px; }
|
||||
.request-live { padding: 12px 14px; border: 1px solid #dbeafe; border-radius: 10px; background: #f8fbff; margin-bottom: 8px; }
|
||||
.model-requests { margin: 10px 0 16px; color: var(--app-text-secondary, #475569); font-size: 12px; }
|
||||
.request-live { padding: 12px 14px; border: 1px solid var(--app-primary-border, #dbeafe); border-radius: 10px; background: var(--app-primary-soft, #f8fbff); margin-bottom: 8px; }
|
||||
.request-heading { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
strong { color: #334155; font-size: 13px; }
|
||||
.model { color: #64748b; overflow-wrap: anywhere; }
|
||||
.elapsed { margin-left: auto; color: #1d4ed8; font-variant-numeric: tabular-nums; }
|
||||
strong { color: var(--app-text-control, #334155); font-size: 13px; }
|
||||
.model { color: var(--app-text-muted, #64748b); overflow-wrap: anywhere; }
|
||||
.elapsed { margin-left: auto; color: var(--app-primary-text, #1d4ed8); font-variant-numeric: tabular-nums; }
|
||||
.status-dot { height: 7px; width: 7px; border-radius: 50%; background: #64748b; flex: 0 0 auto; }
|
||||
.waiting { background: #3b82f6; animation: pulse 1.5s ease-in-out infinite; }
|
||||
.retrying { border-color: #fde68a; background: #fffbeb; .elapsed { color: #92400e; } .waiting { background: #d97706; } }
|
||||
.stale { border-color: #cbd5e1; background: #f8fafc; }
|
||||
.retrying { border-color: var(--app-warning-border, #fde68a); background: var(--app-warning-soft, #fffbeb); .elapsed { color: var(--app-warning-text, #92400e); } .waiting { background: #d97706; } }
|
||||
.stale { border-color: var(--app-border-control, #cbd5e1); background: var(--app-surface-soft, #f8fafc); }
|
||||
p { margin: 6px 0 0; line-height: 1.7; }
|
||||
.error { color: #b45309; overflow-wrap: anywhere; }
|
||||
summary { cursor: pointer; padding: 5px 0; line-height: 1.7; } summary:hover { color: #2563eb; }
|
||||
.error { color: var(--app-warning-text, #b45309); overflow-wrap: anywhere; }
|
||||
summary { cursor: pointer; padding: 5px 0; line-height: 1.7; } summary:hover { color: var(--app-primary-text, #2563eb); }
|
||||
summary:focus-visible, a:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
|
||||
.diagnostics-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin: 8px 0; color: #64748b; }
|
||||
a { color: #2563eb; text-decoration: none; border: 1px solid #dbeafe; border-radius: 6px; padding: 4px 9px; }
|
||||
.diagnostics-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin: 8px 0; color: var(--app-text-muted, #64748b); }
|
||||
a { color: var(--app-primary-text, #2563eb); text-decoration: none; border: 1px solid var(--app-primary-border, #dbeafe); border-radius: 6px; padding: 4px 9px; }
|
||||
.request-history { max-height: 360px; overflow-y: auto; }
|
||||
.request-detail { border-top: 1px solid #e2e8f0; padding: 4px 0; }
|
||||
.request-detail { border-top: 1px solid var(--app-border, #e2e8f0); padding: 4px 0; }
|
||||
dl { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 7px 10px; margin: 8px 0 12px; line-height: 1.6; }
|
||||
dt { color: #64748b; } dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
||||
.image-size, .fallback { display: inline-block; padding: 0 5px; margin: 2px 3px; background: #f1f5f9; border-radius: 4px; }
|
||||
.notice { color: #64748b; }
|
||||
dt { color: var(--app-text-muted, #64748b); } dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
||||
.image-size, .fallback { display: inline-block; padding: 0 5px; margin: 2px 3px; background: var(--app-surface-soft, #f1f5f9); border-radius: 4px; }
|
||||
.notice { color: var(--app-text-muted, #64748b); }
|
||||
@keyframes pulse { 50% { opacity: .35; } }
|
||||
@media (prefers-reduced-motion: reduce) { .waiting { animation: none; } }
|
||||
@media (max-width: 480px) { dl { grid-template-columns: 90px minmax(0, 1fr); } .elapsed { margin-left: 15px; } }
|
||||
+39
-1
@@ -1,7 +1,7 @@
|
||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { FinalReview, LlmRequestStatusComponent, ModelRequest, requestHint, reviewStage } from './llm-request-status.component';
|
||||
import { cancellationReason, FinalReview, LlmRequestStatusComponent, ModelRequest, requestHint, requestTermination, reviewStage } from './llm-request-status.component';
|
||||
|
||||
const waiting: ModelRequest = {
|
||||
request_id: 'call-1', attempt_id: 'attempt-1', model: 'gpt-6-luna', role: 'FlashRunner',
|
||||
@@ -118,4 +118,42 @@ describe('模型请求诊断', () => {
|
||||
expect(reviewStage({ ...review, status: 'cancelled' })).toBe('验收已中断');
|
||||
expect(reviewStage({ ...review, status: 'completed' })).toBe('验收完成');
|
||||
});
|
||||
|
||||
it('取消原因有来源才显示,旧记录与未知字段保留未知', () => {
|
||||
expect(cancellationReason('user_stop')).toContain('用户停止');
|
||||
expect(cancellationReason('guidance_replaced')).toContain('新指导替换');
|
||||
expect(cancellationReason('task_paused')).toContain('任务暂停');
|
||||
expect(cancellationReason('task_replaced')).toContain('新任务替换');
|
||||
expect(cancellationReason('superseded')).toContain('新的检查或计划');
|
||||
expect(cancellationReason()).toContain('取消来源未记录');
|
||||
expect(cancellationReason('untrusted new instruction')).toContain('取消来源未记录');
|
||||
expect(requestTermination({ ...waiting, phase: 'cancelled' })).toContain('取消来源未记录');
|
||||
});
|
||||
|
||||
it('区分三层超时、未完成输出与未确认来源的499', () => {
|
||||
expect(requestTermination({ ...waiting, phase: 'failed', timeout_scope: 'request' })).toContain('单次模型请求');
|
||||
expect(requestTermination({ ...waiting, phase: 'failed', timeout_scope: 'transport' })).toContain('连接或读写超时');
|
||||
expect(requestTermination({ ...waiting, phase: 'failed', timeout_scope: 'final_review' })).toContain('最终验收总预算');
|
||||
expect(requestTermination({ ...waiting, phase: 'cancelled', cancel_reason: 'outer_timeout', timeout_scope: 'outer' })).toContain('外层等待时限');
|
||||
expect(requestTermination({ ...waiting, phase: 'failed', http_status: 499 })).toContain('来源尚未确认');
|
||||
expect(requestTermination({ ...waiting, phase: 'failed', category: 'connection' })).toContain('来源需结合服务端记录');
|
||||
expect(requestTermination({ ...waiting, phase: 'failed', response_guard: { reason: 'missing_completion' } })).toContain('本次输出未被采用');
|
||||
expect(requestTermination({ ...waiting, phase: 'completed' })).toBe('');
|
||||
});
|
||||
|
||||
it('将本次尝试、上次重试、链路编号与结束原因一起展示', () => {
|
||||
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
|
||||
fixture.componentRef.setInput('sessionId', 'cancelled'); fixture.detectChanges();
|
||||
TestBed.inject(HttpTestingController).expectOne('/api/sessions/cancelled/request-diagnostics').flush({ requests: [{
|
||||
...waiting, active: false, phase: 'cancelled', cancel_reason: 'guidance_replaced',
|
||||
cancel_phase: 'retry_wait', previous_attempt_id: 'attempt-before', client_request_id: 'client-wire-id',
|
||||
server_request_id: 'server-wire-id', response_id: 'resp-from-server', server_trace_id: 'gateway-trace',
|
||||
traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01', ended_at: Date.now() / 1000
|
||||
}] });
|
||||
fixture.detectChanges();
|
||||
const text = fixture.nativeElement.textContent;
|
||||
for (const value of ['attempt-before', 'client-wire-id', 'server-wire-id', 'resp-from-server', 'gateway-trace', '新指导替换了旧请求', '中断阶段', '等待重试']) expect(text).toContain(value);
|
||||
expect(fixture.nativeElement.querySelector('.request-live')).toBeNull();
|
||||
fixture.destroy();
|
||||
});
|
||||
});
|
||||
+29
@@ -9,6 +9,10 @@ export interface ModelRequest {
|
||||
attempt: number; max_retries: number; timeout_seconds: number; elapsed_seconds: number;
|
||||
started_at: number; headers_at?: number; first_output_at?: number; last_output_at?: number;
|
||||
ended_at?: number; retry_at?: number; http_status?: number; server_request_id?: string;
|
||||
client_request_id?: string | null; previous_attempt_id?: string | null; response_id?: string;
|
||||
server_trace_id?: string | null; traceparent?: string;
|
||||
cancel_reason?: string; cancel_phase?: string; timeout_scope?: string;
|
||||
response_guard?: { reason: string; status?: string };
|
||||
error?: string; error_type?: string; failure_phase?: string;
|
||||
output_chunks?: number; tool_argument_chars?: number; max_output_tokens?: number | null;
|
||||
tools?: { name: string | null; argument_chars: number }[];
|
||||
@@ -24,6 +28,29 @@ export interface FinalReview {
|
||||
total_seconds: number; elapsed_seconds: number; remaining_seconds: number;
|
||||
request_count: number; retry_count: number; request_attempt?: number; max_retries: number;
|
||||
request_phase?: string; last_failure_category?: string;
|
||||
cancel_reason?: string;
|
||||
}
|
||||
|
||||
export function cancellationReason(reason?: string): string {
|
||||
return ({
|
||||
user_stop: '用户停止了任务。',
|
||||
guidance_replaced: '新指导替换了旧请求,任务可按新指导继续。',
|
||||
task_paused: '任务暂停,已中断当前请求。',
|
||||
task_replaced: '新任务替换了当前任务。',
|
||||
superseded: '新的检查或计划替换了当前请求。',
|
||||
outer_timeout: '外层等待时限已到,取消了当前请求。'
|
||||
} as Record<string, string>)[reason || ''] || '取消来源未记录,无法确定是否由用户停止、指导或外层时限触发。';
|
||||
}
|
||||
|
||||
export function requestTermination(request: ModelRequest): string {
|
||||
if (request.phase === 'cancelled') return cancellationReason(request.cancel_reason);
|
||||
if (request.timeout_scope === 'final_review') return '最终验收总预算已用尽,本次请求已中断。';
|
||||
if (request.timeout_scope === 'request') return '单次模型请求时限已到。';
|
||||
if (request.timeout_scope === 'transport') return '模型连接或读写超时。';
|
||||
if (request.http_status === 499) return '服务返回 HTTP 499;来源尚未确认,需对照本地记录和服务端日志。';
|
||||
if (request.response_guard) return '模型响应未完整结束或工具参数无效,本次输出未被采用。';
|
||||
if (request.category === 'connection') return '模型连接中断或不可用,来源需结合服务端记录核对。';
|
||||
return '';
|
||||
}
|
||||
|
||||
export function reviewStage(review: FinalReview): string {
|
||||
@@ -73,6 +100,8 @@ export class LlmRequestStatusComponent {
|
||||
phase = requestPhase;
|
||||
hint = requestHint;
|
||||
reviewStage = reviewStage;
|
||||
termination = requestTermination;
|
||||
cancellationReason = cancellationReason;
|
||||
|
||||
constructor() {
|
||||
effect(() => this.activeChange.emit(this.active().length > 0 || this.activeReviews().length > 0));
|
||||
|
||||
@@ -24,19 +24,19 @@ let nextId = 0;
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
:host { display: grid; gap: 7px; min-width: 0; color: #334155; font-size: 13px; }
|
||||
:host { display: grid; gap: 7px; min-width: 0; color: var(--app-text-control, #334155); font-size: 13px; }
|
||||
label { font-weight: 600; line-height: 1.5; }
|
||||
.selector { position: relative; min-width: 0; }
|
||||
select, input { box-sizing: border-box; width: 100%; min-width: 0; min-height: 40px;
|
||||
padding: 10px 12px; border: 1px solid #dbe3ec; border-radius: 10px; background: #fff;
|
||||
padding: 10px 12px; border: 1px solid var(--app-border, #dbe3ec); border-radius: 10px; background: var(--app-surface, #fff);
|
||||
color: inherit; font: inherit; }
|
||||
select { appearance: none; padding-right: 36px; cursor: pointer; text-overflow: ellipsis; }
|
||||
.selector > span { position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
|
||||
pointer-events: none; font-size: 19px; color: #64748b; }
|
||||
select:hover:not(:disabled), input:hover:not(:disabled) { border-color: #a5b4fc; }
|
||||
select:focus-visible, input:focus-visible { outline: 2px solid #a5b4fc; outline-offset: 2px; }
|
||||
pointer-events: none; font-size: 19px; color: var(--app-text-muted, #64748b); }
|
||||
select:hover:not(:disabled), input:hover:not(:disabled) { border-color: var(--app-primary-border, #a5b4fc); }
|
||||
select:focus-visible, input:focus-visible { outline: 2px solid var(--app-primary-border, #a5b4fc); outline-offset: 2px; }
|
||||
select:disabled, input:disabled { opacity: .5; cursor: default; }
|
||||
option { background: #fff; color: #334155; }
|
||||
option { background: var(--app-surface, #fff); color: var(--app-text-control, #334155); }
|
||||
`]
|
||||
})
|
||||
export class ModelSelectorComponent {
|
||||
|
||||
+40
-18
@@ -29,7 +29,7 @@ interface Capabilities {
|
||||
template: `
|
||||
<section aria-label="功能开关">
|
||||
<h3>功能开关</h3>
|
||||
<p>按需启用,保存后对尚未开始的任务生效,不影响运行中的任务。</p>
|
||||
<p>Flash 最终 AI 判定切换后自动保存;其余设置点击底部按钮保存。保存后对尚未开始的任务生效,不影响运行中的任务。</p>
|
||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.adb_auto_connect" [disabled]="busy() || loading() || !loaded()" />
|
||||
<span>自动连接设备<small>刷新设备时发现本机常见模拟器,并重连手动连接过的 TCP 设备;重启 ADB 后也会重连。不扫描局域网,不自动选择任务设备。保存后立即生效。</small></span></label>
|
||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.video_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||
@@ -52,7 +52,7 @@ interface Capabilities {
|
||||
}
|
||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.outputter_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||
<span>最终报告生成<small>根据执行记录生成任务结果报告。</small></span></label>
|
||||
<label><input type="checkbox" role="switch" aria-label="Flash 最终 AI 判定与分析" [(ngModel)]="config.flash_final_check_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||
<label><input type="checkbox" role="switch" aria-label="Flash 最终 AI 判定与分析" [ngModel]="config.flash_final_check_enabled" (ngModelChange)="saveFlashFinalCheck($event)" [disabled]="busy() || loading() || !loaded()" />
|
||||
<span>Flash 最终 AI 判定与分析<small>默认关闭。开启后,在 Flash 执行结束时根据用例需求和执行步骤复核结果,会增加耗时。关闭时显示“执行完成 · 未做 AI 判定”,仍保留步骤和报告。不影响 Pro。</small></span></label>
|
||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.step_summarizer_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||
<span>步骤摘要压缩<small>Flash 模式下在后台生成摘要,减少长任务的上下文占用。</small></span></label>
|
||||
@@ -77,33 +77,55 @@ interface Capabilities {
|
||||
@if (error()) { <p role="alert" class="error">{{ error() }}</p> }
|
||||
</section>
|
||||
`,
|
||||
styles: [`section { margin-top: 24px; padding-top: 20px; border-top: 1px solid #64748b55; }
|
||||
h3 { margin: 0 0 8px; } p, small { color: #64748b; line-height: 1.6; }
|
||||
styles: [`section { margin-top: 24px; padding-top: 20px; border-top: 1px solid rgb(var(--app-border-control-rgb, 100 116 139) / 0.3333); }
|
||||
h3 { margin: 0 0 8px; } p, small { color: var(--app-text-muted, #64748b); line-height: 1.6; }
|
||||
label { display: flex; gap: 12px; align-items: flex-start; padding: 14px 0; cursor: pointer; }
|
||||
input { width: 20px; height: 20px; accent-color: #0284c7; flex-shrink: 0; }
|
||||
input { width: 20px; height: 20px; accent-color: var(--app-info-text, #0284c7); flex-shrink: 0; }
|
||||
small { display: block; margin-top: 4px; } button { padding: 10px 18px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; font: inherit; cursor: pointer; }
|
||||
button:disabled { opacity: .5; cursor: default; } .error { color: #be123c; }
|
||||
.execution-settings { margin: 12px 0 20px; padding: 12px 14px; border: 1px solid #cbd5e1; border-radius: 10px; background: #f8fafc; }
|
||||
summary { cursor: pointer; font-weight: 600; color: #334155; }
|
||||
button:disabled { opacity: .5; cursor: default; } .error { color: var(--app-error-text, #be123c); }
|
||||
.execution-settings { margin: 12px 0 20px; padding: 12px 14px; border: 1px solid var(--app-border-control, #cbd5e1); border-radius: 10px; background: var(--app-surface-soft, #f8fafc); }
|
||||
summary { cursor: pointer; font-weight: 600; color: var(--app-text-control, #334155); }
|
||||
.number-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
|
||||
.number-fields label { display: block; padding: 0; color: #334155; }
|
||||
.number-fields input, .number-fields select { box-sizing: border-box; display: block; width: 100%; height: auto; margin-top: 8px; padding: 9px; border: 1px solid #cbd5e1; border-radius: 8px; font: inherit; }
|
||||
section {font-size:13px;border-color:#e2e8f0}h3 {font-size:15px;color:#334155}
|
||||
section > label {align-items:center;flex-direction:row-reverse;justify-content:space-between;border-bottom:1px solid #f1f5f9;gap:20px}section > label > span {flex:1}small {font-size:12px}
|
||||
input[role=switch] {appearance:none;position:relative;width:36px;height:22px;border:1px solid #cbd5e1;border-radius:20px;background:#e2e8f0;cursor:pointer;transition:background .15s}
|
||||
input[role=switch]::after {content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a25;transition:transform .15s}
|
||||
input[role=switch]:checked {background:#6366f1;border-color:#6366f1}input[role=switch]:checked::after {transform:translateX(14px)}input[role=switch]:focus-visible {outline:2px solid #a5b4fc;outline-offset:3px}input:disabled {opacity:.5;cursor:default}
|
||||
button {background:#4f46e5;border-color:#4f46e5;border-radius:12px;font-weight:600}.execution-settings {border-color:#e2e8f0;border-radius:14px;padding:14px 16px}.number-fields input,.number-fields select {background:#fff;border-color:#dbe3ec}
|
||||
.number-fields label { display: block; padding: 0; color: var(--app-text-control, #334155); }
|
||||
.number-fields input, .number-fields select { box-sizing: border-box; display: block; width: 100%; height: auto; margin-top: 8px; padding: 9px; border: 1px solid var(--app-border-control, #cbd5e1); border-radius: 8px; font: inherit; }
|
||||
section {font-size:13px;border-color:var(--app-border, #e2e8f0)}h3 {font-size:15px;color:var(--app-text-control, #334155)}
|
||||
section > label {align-items:center;flex-direction:row-reverse;justify-content:space-between;border-bottom:1px solid var(--app-border, #f1f5f9);gap:20px}section > label > span {flex:1}small {font-size:12px}
|
||||
input[role=switch] {appearance:none;position:relative;width:36px;height:22px;border:1px solid var(--app-border-control, #cbd5e1);border-radius:20px;background:var(--app-surface-raised, #e2e8f0);cursor:pointer;transition:background .15s}
|
||||
input[role=switch]::after {content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--app-surface, #fff);box-shadow:0 1px 3px #0f172a25;transition:transform .15s}
|
||||
input[role=switch]:checked {background:#6366f1;border-color:#6366f1}input[role=switch]:checked::after {transform:translateX(14px)}input[role=switch]:focus-visible {outline:2px solid var(--app-primary-border, #a5b4fc);outline-offset:3px}input:disabled {opacity:.5;cursor:default}
|
||||
button {background:#4f46e5;border-color:#4f46e5;border-radius:12px;font-weight:600}.execution-settings {border-color:var(--app-border, #e2e8f0);border-radius:14px;padding:14px 16px}.number-fields input,.number-fields select {background:var(--app-surface, #fff);border-color:var(--app-border, #dbe3ec)}
|
||||
@media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } }`]
|
||||
})
|
||||
export class CapabilitySettingsComponent implements OnInit {
|
||||
changed = output<Capabilities>();
|
||||
private http = inject(HttpClient);
|
||||
private savedConfig: Capabilities | null = null;
|
||||
config: Capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, video_enabled: false, recording_enabled: false, outputter_enabled: true, flash_final_check_enabled: false, step_summarizer_enabled: true, screenshot_limit_enabled: true, screenshot_max_long_edge: 2048, task_timeout_seconds: 1800, observation_timeout_seconds: 60, observation_max_attempts: 3, device_command_timeout_seconds: 10, action_failure_attempts: 3, worker_cleanup_timeout_seconds: 30 };
|
||||
busy = signal(false); loading = signal(true); loaded = signal(false); message = signal(''); error = signal('');
|
||||
ngOnInit(): void {
|
||||
this.http.get<Capabilities>('/api/system/capability-settings').pipe(timeout(UI_READ_TIMEOUT_MS), finalize(() => this.loading.set(false))).subscribe({
|
||||
next: config => { this.config = config; this.loaded.set(true); this.changed.emit(config); }, error: () => this.error.set('功能配置加载失败,请刷新页面重试。')
|
||||
next: config => { this.savedConfig = structuredClone(config); this.config = config; this.loaded.set(true); this.changed.emit(config); }, error: () => this.error.set('功能配置加载失败,请刷新页面重试。')
|
||||
});
|
||||
}
|
||||
saveFlashFinalCheck(enabled: boolean): void {
|
||||
if (this.busy() || !this.loaded() || !this.savedConfig) return;
|
||||
const previous = this.savedConfig.flash_final_check_enabled;
|
||||
this.config.flash_final_check_enabled = enabled;
|
||||
this.busy.set(true); this.message.set('正在保存 Flash 判定设置…'); this.error.set('');
|
||||
// Preserve other settings that are still being edited, including invalid drafts.
|
||||
const update = { ...this.savedConfig, flash_final_check_enabled: enabled };
|
||||
this.http.post<Capabilities>('/api/system/capability-settings', update).pipe(finalize(() => this.busy.set(false))).subscribe({
|
||||
next: config => {
|
||||
this.savedConfig = structuredClone(config);
|
||||
this.config.flash_final_check_enabled = config.flash_final_check_enabled;
|
||||
this.changed.emit(config);
|
||||
this.message.set(`Flash 最终 AI 判定已${config.flash_final_check_enabled ? '开启' : '关闭'},尚未开始的任务将使用此设置,无需重启服务。`);
|
||||
},
|
||||
error: () => {
|
||||
this.config.flash_final_check_enabled = previous;
|
||||
this.message.set('');
|
||||
this.error.set('Flash 判定设置保存失败,开关已恢复。请检查连接后重新切换。');
|
||||
}
|
||||
});
|
||||
}
|
||||
save(): void {
|
||||
@@ -113,7 +135,7 @@ export class CapabilitySettingsComponent implements OnInit {
|
||||
}
|
||||
this.busy.set(true); this.message.set(''); this.error.set('');
|
||||
this.http.post<Capabilities>('/api/system/capability-settings', this.config).pipe(finalize(() => this.busy.set(false))).subscribe({
|
||||
next: config => { this.config = config; this.changed.emit(config); this.message.set('功能设置已保存,新任务将使用此配置。'); },
|
||||
next: config => { this.savedConfig = structuredClone(config); this.config = config; this.changed.emit(config); this.message.set('功能设置已保存,新任务将使用此配置。'); },
|
||||
error: () => this.error.set('保存失败,请检查服务器连接和 .env 文件权限。')
|
||||
});
|
||||
}
|
||||
|
||||
+27
-4
@@ -221,7 +221,7 @@ describe('持久化模型与功能设置', () => {
|
||||
http.expectNone('/api/system/capability-settings');
|
||||
});
|
||||
|
||||
it('Flash 最终 AI 判定默认关闭,保存失败保留选择,成功后重新打开仍保留', async () => {
|
||||
it('Flash 判定切换即保存,失败恢复原值,重新切换后持久生效', async () => {
|
||||
const fixture = TestBed.createComponent(CapabilitySettingsComponent);
|
||||
const capabilities = { ...fixture.componentInstance.config };
|
||||
fixture.detectChanges();
|
||||
@@ -234,17 +234,20 @@ describe('持久化模型与功能设置', () => {
|
||||
expect(toggle.checked).toBeFalse();
|
||||
expect(root.textContent).toContain('不影响 Pro');
|
||||
toggle.click(); fixture.detectChanges(); await fixture.whenStable();
|
||||
root.querySelector<HTMLButtonElement>('button')!.click();
|
||||
let request = http.expectOne('/api/system/capability-settings');
|
||||
expect(request.request.body.flash_final_check_enabled).toBeTrue();
|
||||
expect(fixture.componentInstance.busy()).toBeTrue();
|
||||
request.flush({}, {status:500,statusText:'Error'});
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.config.flash_final_check_enabled).toBeTrue();
|
||||
await fixture.whenStable();
|
||||
expect(fixture.componentInstance.config.flash_final_check_enabled).toBeFalse();
|
||||
expect(toggle.checked).toBeFalse();
|
||||
expect(fixture.componentInstance.error()).toContain('保存失败');
|
||||
root.querySelector<HTMLButtonElement>('button')!.click();
|
||||
toggle.click(); fixture.detectChanges(); await fixture.whenStable();
|
||||
request = http.expectOne('/api/system/capability-settings');
|
||||
const saved = request.request.body;
|
||||
request.flush(saved);
|
||||
expect(fixture.componentInstance.message()).toContain('无需重启');
|
||||
fixture.destroy();
|
||||
const reopened = TestBed.createComponent(CapabilitySettingsComponent);
|
||||
reopened.detectChanges();
|
||||
@@ -253,4 +256,24 @@ describe('持久化模型与功能设置', () => {
|
||||
expect(reopened.nativeElement.querySelector('[aria-label="Flash 最终 AI 判定与分析"]').checked).toBeTrue();
|
||||
reopened.destroy();
|
||||
});
|
||||
|
||||
it('自动保存 Flash 判定不提交其他未保存或无效的草稿', async () => {
|
||||
const fixture = TestBed.createComponent(CapabilitySettingsComponent);
|
||||
const capabilities = { ...fixture.componentInstance.config };
|
||||
fixture.detectChanges();
|
||||
http.expectOne('/api/system/capability-settings').flush(capabilities);
|
||||
fixture.detectChanges(); await fixture.whenStable();
|
||||
fixture.componentInstance.config.task_timeout_seconds = 0;
|
||||
fixture.componentInstance.config.recording_enabled = true;
|
||||
const toggle: HTMLInputElement = fixture.nativeElement.querySelector('[aria-label="Flash 最终 AI 判定与分析"]');
|
||||
toggle.click(); fixture.detectChanges(); await fixture.whenStable();
|
||||
const request = http.expectOne('/api/system/capability-settings');
|
||||
expect(request.request.body.flash_final_check_enabled).toBeTrue();
|
||||
expect(request.request.body.task_timeout_seconds).toBe(1800);
|
||||
expect(request.request.body.recording_enabled).toBeFalse();
|
||||
request.flush(request.request.body);
|
||||
expect(fixture.componentInstance.config.task_timeout_seconds).toBe(0);
|
||||
expect(fixture.componentInstance.config.recording_enabled).toBeTrue();
|
||||
fixture.destroy();
|
||||
});
|
||||
});
|
||||
@@ -78,27 +78,27 @@ interface ModelSettings {
|
||||
</form>
|
||||
`,
|
||||
styles: [`
|
||||
.model-settings { display: grid; gap: 10px; color: #1e293b; }
|
||||
p, small { margin: 0; line-height: 1.6; color: #64748b; }
|
||||
.model-settings { display: grid; gap: 10px; color: var(--app-text-control, #1e293b); }
|
||||
p, small { margin: 0; line-height: 1.6; color: var(--app-text-muted, #64748b); }
|
||||
label { font-weight: 600; } label span { font-weight: 400; font-size: 12px; }
|
||||
input, select { box-sizing: border-box; width: 100%; padding: 12px; border-radius: 10px; border: 1px solid #64748b66;
|
||||
background: #f8fafc; color: inherit; font: inherit; }
|
||||
input, select { box-sizing: border-box; width: 100%; padding: 12px; border-radius: 10px; border: 1px solid rgb(var(--app-border-control-rgb, 100 116 139) / 0.4000);
|
||||
background: var(--app-surface-soft, #f8fafc); color: inherit; font: inherit; }
|
||||
input:focus, select:focus { outline: 2px solid #38bdf8; }
|
||||
.number-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; }
|
||||
.number-fields input { margin-top: 8px; }
|
||||
.recovery-settings { border: 1px solid #cbd5e1; border-radius: 10px; background: #f8fafc; padding: 10px 14px; }
|
||||
.recovery-settings summary { cursor: pointer; font-weight: 600; color: #334155; }
|
||||
.recovery-settings { border: 1px solid var(--app-border-control, #cbd5e1); border-radius: 10px; background: var(--app-surface-soft, #f8fafc); padding: 10px 14px; }
|
||||
.recovery-settings summary { cursor: pointer; font-weight: 600; color: var(--app-text-control, #334155); }
|
||||
.recovery-content { display: grid; gap: 10px; padding-top: 12px; }
|
||||
.recovery-content label input, .recovery-content label select { margin-top: 8px; }
|
||||
.actions { display: flex; gap: 12px; margin-top: 8px; }
|
||||
button { padding: 11px 20px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; cursor: pointer; font: inherit; }
|
||||
button:disabled { opacity: .5; cursor: default; } .success { color: #047857; } .error { color: #be123c; }
|
||||
button:disabled { opacity: .5; cursor: default; } .success { color: var(--app-success-text, #047857); } .error { color: var(--app-error-text, #be123c); }
|
||||
button:focus-visible { outline: 2px solid #0284c7; outline-offset: 2px; }
|
||||
small { overflow-wrap: anywhere; } @media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } }
|
||||
.model-settings {gap:12px;font-size:13px} label {margin-top:6px}input,select {background:#fff;border-color:#dbe3ec;padding:11px 12px}
|
||||
input:focus,select:focus {outline:2px solid #a5b4fc;outline-offset:2px}
|
||||
.actions {border-top:1px solid #e2e8f0;padding-top:16px;align-items:center}.actions button {min-height:40px;background:#4f46e5;border-color:#4f46e5;font-weight:600;border-radius:12px}.actions button[type=button] {background:#fff;border-color:#dbe3ec;color:#475569}
|
||||
.recovery-settings {border-color:#e2e8f0;border-radius:14px;padding:14px 16px}.recovery-settings summary {font-size:13px}
|
||||
.model-settings {gap:12px;font-size:13px} label {margin-top:6px}input,select {background:var(--app-surface, #fff);border-color:var(--app-border, #dbe3ec);padding:11px 12px}
|
||||
input:focus,select:focus {outline:2px solid var(--app-primary-border, #a5b4fc);outline-offset:2px}
|
||||
.actions {border-top:1px solid var(--app-border, #e2e8f0);padding-top:16px;align-items:center}.actions button {min-height:40px;background:#4f46e5;border-color:#4f46e5;font-weight:600;border-radius:12px}.actions button[type=button] {background:var(--app-surface, #fff);border-color:var(--app-border, #dbe3ec);color:var(--app-text-secondary, #475569)}
|
||||
.recovery-settings {border-color:var(--app-border, #e2e8f0);border-radius:14px;padding:14px 16px}.recovery-settings summary {font-size:13px}
|
||||
`]
|
||||
})
|
||||
export class ModelSettingsComponent implements OnInit {
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.72);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
border: 1px solid rgb(var(--app-border-rgb, 226 232 240) / 0.85);
|
||||
border-radius: 18px;
|
||||
padding: 3px;
|
||||
gap: 3px;
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.9);
|
||||
user-select: none;
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
@@ -38,15 +38,15 @@
|
||||
border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 4px 14px -2px rgba(15, 23, 42, 0.1), 0 2px 4px rgba(0, 0, 0, 0.03), inset 0 1px 1px rgba(255, 255, 255, 1);
|
||||
border-color: rgba(203, 213, 225, 0.9);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.85);
|
||||
box-shadow: 0 4px 14px -2px rgba(15, 23, 42, 0.1), 0 2px 4px rgba(0, 0, 0, 0.03), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 1);
|
||||
border-color: rgb(var(--app-border-control-rgb, 203 213 225) / 0.9);
|
||||
}
|
||||
|
||||
.app-version {
|
||||
padding: 0 10px 0 8px;
|
||||
border-left: 1px solid #e2e8f0;
|
||||
color: #64748b;
|
||||
border-left: 1px solid var(--app-border, #e2e8f0);
|
||||
color: var(--app-text-muted, #64748b);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
@@ -65,7 +65,7 @@
|
||||
border-radius: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
@@ -83,24 +83,24 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
color: var(--app-text-muted, #64748b);
|
||||
transition: color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
color: #475569;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: #0f172a;
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.7);
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
|
||||
.tab-icon {
|
||||
color: #1a73e8;
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
.tab-label {
|
||||
color: #0f172a;
|
||||
color: var(--app-text-strong, #0f172a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
color: #ffffff !important;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(26, 115, 232, 0.35), inset 0 1px 1px rgba(255, 255, 255, 0.35);
|
||||
box-shadow: 0 2px 8px rgba(26, 115, 232, 0.35), inset 0 1px 1px rgb(var(--app-highlight-rgb, 255 255 255) / 0.35);
|
||||
|
||||
.tab-icon,
|
||||
.tab-label,
|
||||
@@ -124,3 +124,40 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--app-text-secondary, #475569);
|
||||
cursor: pointer;
|
||||
|
||||
.material-symbols-outlined { font-size: 16px; }
|
||||
&:hover {
|
||||
background: var(--app-surface-raised, #eef2f7);
|
||||
color: var(--app-primary-text, #1a73e8);
|
||||
}
|
||||
}
|
||||
|
||||
.floating-nav-switcher .nav-tab-btn:focus-visible,
|
||||
.theme-toggle:focus-visible {
|
||||
outline: 2px solid var(--app-primary-text, #1a73e8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.floating-nav-switcher {
|
||||
left: 12px;
|
||||
max-width: calc(100vw - 24px);
|
||||
.nav-tab-btn { padding: 0 10px; }
|
||||
.nav-tab-btn .tab-label { display: none; }
|
||||
.app-version { padding: 0 6px; }
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { version } from '../../../../package.json';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-switcher',
|
||||
@@ -30,6 +31,7 @@ import { version } from '../../../../package.json';
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{exact: true}"
|
||||
class="nav-tab-btn"
|
||||
aria-label="新任务 / 首页"
|
||||
title="返回首页开始新任务"
|
||||
>
|
||||
<span class="material-symbols-outlined tab-icon">add_task</span>
|
||||
@@ -39,15 +41,22 @@ import { version } from '../../../../package.json';
|
||||
routerLink="/workspace"
|
||||
routerLinkActive="active"
|
||||
class="nav-tab-btn"
|
||||
aria-label="工作区"
|
||||
title="打开工作区"
|
||||
>
|
||||
<span class="material-symbols-outlined tab-icon">space_dashboard</span>
|
||||
<span class="tab-label">工作区</span>
|
||||
</a>
|
||||
<a routerLink="/plugins" routerLinkActive="active" class="nav-tab-btn" title="管理扩展插件">
|
||||
<a routerLink="/plugins" routerLinkActive="active" class="nav-tab-btn" aria-label="插件" title="管理扩展插件">
|
||||
<span class="material-symbols-outlined tab-icon">extension</span><span class="tab-label">插件</span>
|
||||
</a>
|
||||
<span class="app-version" [title]="'ARTEMIS 版本 ' + version" [attr.aria-label]="'ARTEMIS 版本 ' + version">v{{version}}</span>
|
||||
<button type="button" class="theme-toggle"
|
||||
[attr.aria-label]="theme.isDark() ? '切换为浅色模式' : '切换为暗色模式'"
|
||||
[title]="theme.isDark() ? '切换为浅色模式' : '切换为暗色模式'"
|
||||
(click)="theme.toggle()">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">{{ theme.isDark() ? 'light_mode' : 'dark_mode' }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
@@ -55,4 +64,5 @@ import { version } from '../../../../package.json';
|
||||
})
|
||||
export class NavSwitcherComponent {
|
||||
readonly version = version;
|
||||
readonly theme = inject(ThemeService);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { PluginToolResultComponent } from './plugin-tool-result.component';
|
||||
import { PluginToolView } from '../../utils/plugin-tool-view.util';
|
||||
|
||||
describe('readable plugin receipts', () => {
|
||||
const makeView = (result: string): PluginToolView => ({
|
||||
title: '观察电脑窗口', kind: '辅助观察', auxiliary: true, status: '已执行', args: [],
|
||||
result, rawResult: JSON.stringify({ result, call_id: 'original-call' }, null, 2),
|
||||
facts: [{ key: '窗口', value: 'Demo' }, { key: '截图', value: '1280 × 720' }]
|
||||
});
|
||||
const mount = async (view: PluginToolView) => {
|
||||
await TestBed.configureTestingModule({ imports: [PluginToolResultComponent] }).compileComponents();
|
||||
const fixture = TestBed.createComponent(PluginToolResultComponent);
|
||||
fixture.componentRef.setInput('view', view);
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
};
|
||||
|
||||
it('keeps a long control tree and the original payload folded and fully accessible', async () => {
|
||||
const fullText = Array.from({ length: 200 }, (_, i) => `- [${i}] Button "Item ${i}"`).join('\n');
|
||||
const view = makeView(fullText);
|
||||
const fixture = await mount(view);
|
||||
const root: HTMLElement = fixture.nativeElement;
|
||||
expect(root.querySelector('dl')!.textContent).toContain('1280 × 720');
|
||||
expect(root.querySelector('.receipt-preview')!.textContent!.length).toBeLessThan(1100);
|
||||
const textDetails = root.querySelector<HTMLDetailsElement>('.receipt-text')!;
|
||||
const rawDetails = root.querySelector<HTMLDetailsElement>('.receipt-raw')!;
|
||||
expect(textDetails.open).toBeFalse();
|
||||
expect(rawDetails.open).toBeFalse();
|
||||
textDetails.querySelector('summary')!.click();
|
||||
expect(textDetails.open).toBeTrue();
|
||||
expect(textDetails.querySelector('pre')!.textContent).toBe(fullText);
|
||||
expect(rawDetails.querySelector('pre')!.textContent).toBe(view.rawResult);
|
||||
});
|
||||
|
||||
it('renders untrusted tool text as text and keeps short results readable', async () => {
|
||||
const text = '<img src=x onerror="alert(1)">\n已观察,未验证业务结果';
|
||||
const fixture = await mount(makeView(text));
|
||||
const root: HTMLElement = fixture.nativeElement;
|
||||
expect(root.querySelector('.receipt-preview')!.textContent).toBe(text);
|
||||
expect(root.querySelector('img')).toBeNull();
|
||||
expect(root.querySelector('.receipt-text')).toBeNull();
|
||||
});
|
||||
|
||||
it('reflects a new receipt without resetting an open raw payload', async () => {
|
||||
const fixture = await mount(makeView('执行中'));
|
||||
const raw = fixture.nativeElement.querySelector('.receipt-raw') as HTMLDetailsElement;
|
||||
raw.querySelector('summary')!.click();
|
||||
fixture.componentRef.setInput('view', makeView('已返回新的窗口观察'));
|
||||
fixture.detectChanges();
|
||||
expect(raw.open).toBeTrue();
|
||||
expect(fixture.nativeElement.querySelector('.receipt-preview').textContent).toBe('已返回新的窗口观察');
|
||||
expect(raw.querySelector('pre')!.textContent).toContain('已返回新的窗口观察');
|
||||
});
|
||||
|
||||
it('keeps long window labels and unbroken receipt text inside a narrow container', async () => {
|
||||
const view = makeView('long-unbroken-result'.repeat(200));
|
||||
view.facts[0].value = 'VeryLongWindowTitle'.repeat(30);
|
||||
const fixture = await mount(view);
|
||||
const root = fixture.nativeElement as HTMLElement;
|
||||
root.style.width = '240px';
|
||||
fixture.detectChanges();
|
||||
expect(root.clientWidth).toBe(240);
|
||||
expect(root.scrollWidth).toBeLessThanOrEqual(root.clientWidth);
|
||||
const label = root.querySelector('dd')!;
|
||||
expect(label.scrollWidth).toBeLessThanOrEqual(label.clientWidth);
|
||||
});
|
||||
});
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { PluginToolView } from '../../utils/plugin-tool-view.util';
|
||||
|
||||
/** Readable presentation only: the full receipt and model context stay intact. */
|
||||
@Component({
|
||||
selector: 'app-plugin-tool-result',
|
||||
standalone: true,
|
||||
template: `
|
||||
@if (view().facts.length) {
|
||||
<dl class="receipt-facts">
|
||||
@for (fact of view().facts; track fact.key) {
|
||||
<div><dt>{{ fact.key }}</dt><dd>{{ fact.value }}</dd></div>
|
||||
}
|
||||
</dl>
|
||||
}
|
||||
@if (view().result) {
|
||||
<pre class="receipt-preview">{{ preview() }}</pre>
|
||||
@if (isLong()) {
|
||||
<details class="receipt-detail receipt-text">
|
||||
<summary>完整文本与控件树 <span>{{ view().result.length }} 字符</span></summary>
|
||||
<pre>{{ view().result }}</pre>
|
||||
</details>
|
||||
}
|
||||
}
|
||||
@if (view().rawResult) {
|
||||
<details class="receipt-detail receipt-raw">
|
||||
<summary>原始返回 payload <span>完整保留</span></summary>
|
||||
<pre>{{ view().rawResult }}</pre>
|
||||
</details>
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
:host { display: block; min-width: 0; width: 100%; user-select: text; cursor: auto; }
|
||||
.receipt-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr)); gap: 8px 16px; margin: 0 0 12px; }
|
||||
.receipt-facts div { min-width: 0; }
|
||||
dt { font-size: 11px; color: var(--app-text-muted, #64748b); }
|
||||
dd { margin: 3px 0 0; font-size: 12px; color: var(--app-text-control, #334155); overflow-wrap: anywhere; }
|
||||
pre { margin: 0; padding: 10px 12px; border-radius: 6px; font: 12px/1.6 Consolas, monospace; color: var(--app-text-control, #334155); background: var(--app-surface-soft, #f8fafc); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.receipt-preview { max-height: 180px; overflow: auto; }
|
||||
.receipt-detail { margin-top: 10px; border: 1px solid var(--app-border, #e2e8f0); border-radius: 6px; min-width: 0; }
|
||||
summary { padding: 9px 10px; font-size: 12px; color: var(--app-text-secondary, #475569); cursor: pointer; overflow-wrap: anywhere; }
|
||||
summary span { margin-left: 8px; font-size: 11px; color: var(--app-text-muted, #64748b); }
|
||||
summary:focus-visible { outline: 2px solid var(--app-primary-text, #1a73e8); outline-offset: 2px; }
|
||||
.receipt-detail pre { max-height: 400px; overflow: auto; border-top: 1px solid var(--app-border, #e2e8f0); border-radius: 0 0 6px 6px; }
|
||||
`]
|
||||
})
|
||||
export class PluginToolResultComponent {
|
||||
view = input.required<PluginToolView>();
|
||||
private readonly previewLimit = 1000;
|
||||
preview = computed(() => {
|
||||
const text = this.view().result;
|
||||
const excerpt = text.split('\n').slice(0, 7).join('\n').slice(0, this.previewLimit);
|
||||
return excerpt.length < text.length ? `${excerpt}\n…` : text;
|
||||
});
|
||||
isLong = computed(() => this.preview() !== this.view().result);
|
||||
}
|
||||
+4
-1
@@ -4,7 +4,7 @@ import { ReportMarkdownComponent } from './report-markdown.component';
|
||||
describe('报告 Markdown 展示', () => {
|
||||
it('renders report headings, emphasis, lists, tables and code in both preview and full view', () => {
|
||||
const fixture = TestBed.createComponent(ReportMarkdownComponent);
|
||||
fixture.componentRef.setInput('content', '### 双端布局报告\n- **手机**:`/zh`\n\n| 区域 | 结论 |\n| --- | --- |\n| 页底 | 已确认 |\n\n```text\n<原始证据>\n```');
|
||||
fixture.componentRef.setInput('content', '### 双端布局报告\n- **手机**:`/zh`\n- [x] 已检查\n- [/] 进行中\n\n| 区域 | 结论 |\n| --- | ---: |\n| 页底 | 已确认 |\n\n```text\n<原始证据>\n```');
|
||||
fixture.componentRef.setInput('compact', true);
|
||||
fixture.detectChanges();
|
||||
const view: HTMLElement = fixture.nativeElement;
|
||||
@@ -12,6 +12,9 @@ describe('报告 Markdown 展示', () => {
|
||||
expect(view.querySelector('li strong')?.textContent).toBe('手机');
|
||||
expect(view.querySelector('li code')?.textContent).toBe('/zh');
|
||||
expect(view.querySelector('td')?.textContent).toBe('页底');
|
||||
expect(view.querySelector('td.md-align-right')?.textContent).toBe('已确认');
|
||||
expect(view.querySelector('[aria-label="已完成"]')).not.toBeNull();
|
||||
expect(view.querySelector('[aria-label="进行中"]')).not.toBeNull();
|
||||
expect(view.querySelector('pre code')?.textContent).toBe('<原始证据>');
|
||||
expect(view.querySelector('.compact')).not.toBeNull();
|
||||
fixture.componentRef.setInput('compact', false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { renderMarkdownToHtml } from '../../utils/markdown-parser.util';
|
||||
|
||||
/** Shared, sanitized presentation for report prose (not the editable task plan). */
|
||||
/** Shared presentation for notes, plans and reports; Angular sanitizes the HTML. */
|
||||
@Component({
|
||||
selector: 'app-report-markdown',
|
||||
standalone: true,
|
||||
@@ -14,21 +14,21 @@ import { renderMarkdownToHtml } from '../../utils/markdown-parser.util';
|
||||
> div, p { margin: 4px 0; }
|
||||
> :first-child { margin-top: 0; }
|
||||
> :last-child { margin-bottom: 0; }
|
||||
h1,h2,h3,h4,h5,h6 { margin: 16px 0 6px; font-weight: 600; line-height: 1.5; color: #0f172a; }
|
||||
h1,h2,h3,h4,h5,h6 { margin: 16px 0 6px; font-weight: 600; line-height: 1.5; color: var(--app-text-strong, #0f172a); }
|
||||
h1 { font-size: 18px; } h2 { font-size: 16px; } h3,h4,h5,h6 { font-size: 14px; }
|
||||
strong { font-weight: 600; }
|
||||
ul,ol { margin: 6px 0; padding-left: 24px; }
|
||||
li { margin: 4px 0; }
|
||||
blockquote { margin: 10px 0; padding: 6px 12px; border-left: 3px solid #93c5fd; background: #f8fafc; }
|
||||
code { font-family: Consolas, monospace; font-size: .92em; border-radius: 4px; padding: 1px 4px; background: #f1f5f9; }
|
||||
pre { max-width: 100%; overflow: auto; padding: 10px 12px; border-radius: 6px; background: #f1f5f9; white-space: pre; }
|
||||
blockquote { margin: 10px 0; padding: 6px 12px; border-left: 3px solid var(--app-primary-border, #93c5fd); background: var(--app-surface-soft, #f8fafc); }
|
||||
code { font-family: Consolas, monospace; font-size: .92em; border-radius: 4px; padding: 1px 4px; background: var(--app-surface-soft, #f1f5f9); }
|
||||
pre { max-width: 100%; overflow: auto; padding: 10px 12px; border-radius: 6px; background: var(--app-surface-soft, #f1f5f9); white-space: pre; }
|
||||
pre code { padding: 0; background: none; }
|
||||
.md-table-scroll { max-width: 100%; overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th,td { border: 1px solid #cbd5e1; padding: 7px 9px; text-align: left; vertical-align: top; }
|
||||
th { background: #eff6ff; font-weight: 600; }
|
||||
th,td { border: 1px solid var(--app-border-control, #cbd5e1); padding: 7px 9px; text-align: left; vertical-align: top; }
|
||||
th { background: var(--app-primary-soft, #eff6ff); font-weight: 600; }
|
||||
.md-para-gap { height: 4px; margin: 0; }
|
||||
hr { border: 0; border-top: 1px solid #e2e8f0; margin: 12px 0; }
|
||||
hr { border: 0; border-top: 1px solid var(--app-border, #e2e8f0); margin: 12px 0; }
|
||||
}
|
||||
`]
|
||||
})
|
||||
|
||||
@@ -12,13 +12,13 @@ import { Component, computed, input, output } from '@angular/core';
|
||||
@if(hint()){<p>{{hint()}}</p>}
|
||||
`,
|
||||
styles: [`
|
||||
:host{display:block;min-width:0;padding:14px 16px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;font-size:13px;color:#334155}
|
||||
:host{display:block;min-width:0;padding:14px 16px;background:var(--app-surface-soft, #f8fafc);border:1px solid var(--app-border, #e2e8f0);border-radius:14px;font-size:13px;color:var(--app-text-control, #334155)}
|
||||
.slider-heading,.slider-ends{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
||||
.slider-heading{font-weight:600}output{color:#4f46e5;font-size:12px;white-space:nowrap}.slider-ends{font-size:11px;color:#64748b}
|
||||
input{appearance:none;display:block;box-sizing:border-box;width:100%;height:6px;border:0;border-radius:10px;margin:20px 0 12px;background:linear-gradient(to right,#6366f1 var(--fill),#dbe3ee var(--fill));cursor:pointer}
|
||||
input::-webkit-slider-thumb{appearance:none;width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid #6366f1;box-shadow:0 2px 6px #33415526}
|
||||
input::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#fff;border:2px solid #6366f1}
|
||||
input:focus-visible{outline:2px solid #a5b4fc;outline-offset:5px}input:disabled{opacity:.5;cursor:default}p{margin:10px 0 0;font-size:12px;line-height:1.6;color:#64748b}
|
||||
.slider-heading{font-weight:600}output{color:var(--app-exploration-text, #4f46e5);font-size:12px;white-space:nowrap}.slider-ends{font-size:11px;color:var(--app-text-muted, #64748b)}
|
||||
input{appearance:none;display:block;box-sizing:border-box;width:100%;height:6px;border:0;border-radius:10px;margin:20px 0 12px;background:linear-gradient(to right,#6366f1 var(--fill),var(--app-primary-soft, #dbe3ee) var(--fill));cursor:pointer}
|
||||
input::-webkit-slider-thumb{appearance:none;width:18px;height:18px;border-radius:50%;background:var(--app-surface, #fff);border:2px solid #6366f1;box-shadow:0 2px 6px #33415526}
|
||||
input::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--app-surface, #fff);border:2px solid #6366f1}
|
||||
input:focus-visible{outline:2px solid var(--app-primary-border, #a5b4fc);outline-offset:5px}input:disabled{opacity:.5;cursor:default}p{margin:10px 0 0;font-size:12px;line-height:1.6;color:var(--app-text-muted, #64748b)}
|
||||
`]
|
||||
})
|
||||
export class SettingSliderComponent {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { AgentService } from '../../services/agent.service';
|
||||
@if(agent.batchFilter() && !['__batch__','__single__','__exploration__'].includes(agent.batchFilter()) && !hasSelectedBatch()){<option [value]="agent.batchFilter()">当前选定批次</option>}
|
||||
</select></label>@if(agent.batchFilter()){<button type="button" (click)="agent.batchFilter.set('')">清除筛选</button>}</div>
|
||||
@if(error()){<div class="filter-error" role="alert">{{error()}} <button type="button" (click)="load()">重试</button></div>}`,
|
||||
styles: [`:host{display:block;margin:0 0 12px}.batch-filter{display:flex;align-items:flex-end;gap:8px;color:#475569;font-size:12px}label{flex:1;min-width:0}select{display:block;box-sizing:border-box;width:100%;height:34px;margin-top:5px;border:1px solid #cbd5e1;border-radius:7px;padding:0 9px;background:#fff;color:#1e293b;font:inherit}option{background:#fff;color:#1e293b}button{height:34px;white-space:nowrap;border:1px solid #cbd5e1;border-radius:7px;padding:0 9px;background:#fff;color:#475569;font:inherit;cursor:pointer}select:focus-visible,button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}.filter-error{font-size:12px;color:#b91c1c;margin-top:5px}.filter-error button{height:auto;padding:2px 6px}`]
|
||||
styles: [`:host{display:block;margin:0 0 12px}.batch-filter{display:flex;align-items:flex-end;gap:8px;color:var(--app-text-secondary, #475569);font-size:12px}label{flex:1;min-width:0}select{display:block;box-sizing:border-box;width:100%;height:34px;margin-top:5px;border:1px solid var(--app-border-control, #cbd5e1);border-radius:7px;padding:0 9px;background:var(--app-surface, #fff);color:var(--app-text-control, #1e293b);font:inherit}option{background:var(--app-surface, #fff);color:var(--app-text-control, #1e293b)}button{height:34px;white-space:nowrap;border:1px solid var(--app-border-control, #cbd5e1);border-radius:7px;padding:0 9px;background:var(--app-surface, #fff);color:var(--app-text-secondary, #475569);font:inherit;cursor:pointer}select:focus-visible,button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}.filter-error{font-size:12px;color:var(--app-error-text, #b91c1c);margin-top:5px}.filter-error button{height:auto;padding:2px 6px}`]
|
||||
})
|
||||
export class TaskBatchFilterComponent {
|
||||
agent = inject(AgentService); private http = inject(HttpClient);
|
||||
|
||||
@@ -25,9 +25,9 @@ import { AgentService } from '../../services/agent.service';
|
||||
h3{display:flex;align-items:center;gap:6px;font-size:14px;margin:14px 0 8px}
|
||||
.query-status{margin-left:auto;font-size:11px;font-weight:400;white-space:nowrap}
|
||||
small{opacity:.6}
|
||||
input,select,button{font:inherit;color:inherit;border:1px solid #64748b55;border-radius:6px;padding:8px;background:transparent;box-sizing:border-box}
|
||||
input,select,button{font:inherit;color:inherit;border:1px solid rgb(var(--app-border-control-rgb, 100 116 139) / 0.3333);border-radius:6px;padding:8px;background:transparent;box-sizing:border-box}
|
||||
input{width:100%}
|
||||
select option{background:white;color:#1e293b}
|
||||
select option{background:var(--app-surface, white);color:var(--app-text-control, #1e293b)}
|
||||
.filters{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}
|
||||
.filters label{flex:1;min-width:110px}
|
||||
.filters input,.filters select{display:block;width:100%}
|
||||
@@ -35,26 +35,26 @@ import { AgentService } from '../../services/agent.service';
|
||||
.row{display:flex;flex-direction:column;gap:6px;width:100%;text-align:left;cursor:pointer;margin:5px 0;overflow-wrap:anywhere}
|
||||
.row strong{font-weight:600;display:flex;align-items:center;min-height:28px;padding-left:33px}
|
||||
.row.selected{border-color:#60a5fa;background:#3b82f615}
|
||||
.history-item{position:relative;border:1px solid #cbd5e1;border-left:3px solid #94a3b8;border-radius:8px;margin:6px 0;padding:8px;background:#fff}
|
||||
.history-item{position:relative;border:1px solid var(--app-border-control, #cbd5e1);border-left:3px solid var(--app-border-control, #94a3b8);border-radius:8px;margin:6px 0;padding:8px;background:var(--app-surface, #fff)}
|
||||
.history-item.status-completed,.history-item.status-success{border-left-color:#16a34a}
|
||||
.history-item.status-failed{border-left-color:#dc2626;background:#fffafa}
|
||||
.history-item.status-cancelled,.history-item.status-interrupted{border-left-color:#d97706;background:#fffdf8}
|
||||
.history-item.status-skipped{border-left-color:#64748b}
|
||||
.history-item:has(.row.selected){border-color:#60a5fa;background:#eff6ff}
|
||||
.history-item.status-failed{border-left-color:#dc2626;background:var(--app-error-soft, #fffafa)}
|
||||
.history-item.status-cancelled,.history-item.status-interrupted{border-left-color:#d97706;background:var(--app-warning-soft, #fffdf8)}
|
||||
.history-item.status-skipped{border-left-color:var(--app-border-control, #64748b)}
|
||||
.history-item:has(.row.selected){border-color:#60a5fa;background:var(--app-primary-soft, #eff6ff)}
|
||||
.history-item .row{border:0;padding:0 35px 0 0;margin:0 0 4px}
|
||||
.history-item .row.selected{background:transparent}
|
||||
.history-heading{position:relative;min-width:0}
|
||||
.history-heading>app-favorite-button{position:absolute;left:0;top:0;z-index:1}
|
||||
.history-heading>.row{min-width:0;width:100%}
|
||||
.history-meta{display:flex;gap:5px;align-items:center;flex-wrap:wrap;color:#475569;font-size:11px}
|
||||
.history-status{display:inline-flex;align-items:center;box-sizing:border-box;height:20px;line-height:14px;border-radius:5px;padding:0 6px;background:#f1f5f9;color:#334155}
|
||||
.status-completed .history-status,.status-success .history-status{background:#dcfce7;color:#166534}
|
||||
.status-failed .history-status{background:#fee2e2;color:#991b1b}
|
||||
.status-cancelled .history-status,.status-interrupted .history-status{background:#fef3c7;color:#92400e}
|
||||
.history-error{font-size:11px;color:#b91c1c;line-height:1.4}
|
||||
.delete-history{position:absolute;right:5px;top:8px;border:0;padding:3px;color:#64748b;cursor:pointer;font-size:12px}
|
||||
.history-meta{display:flex;gap:5px;align-items:center;flex-wrap:wrap;color:var(--app-text-secondary, #475569);font-size:11px}
|
||||
.history-status{display:inline-flex;align-items:center;box-sizing:border-box;height:20px;line-height:14px;border-radius:5px;padding:0 6px;background:var(--app-surface-soft, #f1f5f9);color:var(--app-text-control, #334155)}
|
||||
.status-completed .history-status,.status-success .history-status{background:var(--app-success-soft, #dcfce7);color:var(--app-success-text, #166534)}
|
||||
.status-failed .history-status{background:var(--app-error-soft, #fee2e2);color:var(--app-error-text, #991b1b)}
|
||||
.status-cancelled .history-status,.status-interrupted .history-status{background:var(--app-warning-soft, #fef3c7);color:var(--app-warning-text, #92400e)}
|
||||
.history-error{font-size:11px;color:var(--app-error-text, #b91c1c);line-height:1.4}
|
||||
.delete-history{position:absolute;right:5px;top:8px;border:0;padding:3px;color:var(--app-text-muted, #64748b);cursor:pointer;font-size:12px}
|
||||
.delete-history .material-symbols-outlined{font-size:16px}
|
||||
.delete-history:hover{color:#b91c1c}
|
||||
.delete-history:hover{color:var(--app-error-text, #b91c1c)}
|
||||
.pager{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:12px 0}
|
||||
button:disabled{opacity:.4}
|
||||
p{opacity:.7}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Component, DestroyRef, Input, inject, signal } from '@angular/core';
|
||||
padding: 3px 0; cursor: pointer; user-select: text; border-radius: 3px; }
|
||||
.value { min-width: 0; overflow-wrap: anywhere; user-select: text; }
|
||||
.material-symbols-outlined { flex-shrink: 0; font-size: 14px; user-select: none; }
|
||||
.copy-target:hover { color: #2563eb; }
|
||||
.copy-target:hover { color: var(--app-primary-text, #2563eb); }
|
||||
.copy-target:focus-visible { outline: 2px solid #60a5fa; outline-offset: 2px; }
|
||||
.feedback { display: block; line-height: 1.6; user-select: text; }
|
||||
.feedback:empty { display: none; }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TaskLifecycleComponent, durationText } from './task-lifecycle.component';
|
||||
|
||||
describe('TaskLifecycleComponent', () => {
|
||||
it('distinguishes missing time, zero and longer durations', () => {
|
||||
expect(durationText(null)).toBe('未记录');
|
||||
expect(durationText(0)).toBe('0 秒');
|
||||
expect(durationText(125.7)).toBe('2 分 5 秒');
|
||||
});
|
||||
|
||||
it('shows final review separately and never turns execution completion into a passed verdict', () => {
|
||||
const fixture = TestBed.createComponent(TaskLifecycleComponent);
|
||||
fixture.componentRef.setInput('value', {
|
||||
schema_version:1, status:'completed', phase:'finished', observed_at:100,
|
||||
durations:{queued:60, resource_wait:5, execution:100, final_review:20, paused:null}
|
||||
});
|
||||
fixture.detectChanges();
|
||||
const text = fixture.nativeElement.textContent;
|
||||
expect(text).toContain('执行已结束');
|
||||
expect(text).toContain('1 分 0 秒');
|
||||
expect(text).toContain('最终验收');
|
||||
expect(text).toContain('20 秒');
|
||||
expect(text).toContain('未记录');
|
||||
expect(text).toContain('执行完成不等于验证通过');
|
||||
expect(fixture.nativeElement.querySelector('app-test-result-badge')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not invent lifecycle data for an older backend', () => {
|
||||
const fixture = TestBed.createComponent(TaskLifecycleComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('details')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { TaskLifecycle } from '../../core/models/session.model';
|
||||
|
||||
export function durationText(value: number | null | undefined): string {
|
||||
if (value == null || !Number.isFinite(value)) return '未记录';
|
||||
const seconds = Math.max(0, Math.floor(value));
|
||||
return seconds < 60 ? `${seconds} 秒` : `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒`;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-task-lifecycle', standalone: true, imports: [DatePipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `@if (value(); as progress) {
|
||||
<details class="lifecycle">
|
||||
<summary>任务进度 · {{ phase(progress.phase) }}</summary>
|
||||
<dl>
|
||||
@for (item of items; track item.key) {
|
||||
<div><dt>{{ item.label }}</dt><dd>{{ duration(progress.durations[item.key]) }}</dd></div>
|
||||
}
|
||||
</dl>
|
||||
<p>{{ progress.note || '分项使用实际记录;缺失为未知。执行完成不等于验证通过。' }}</p>
|
||||
@if (progress.observed_at != null) { <small>记录更新于 {{ progress.observed_at * 1000 | date:'HH:mm:ss' }}</small> }
|
||||
</details>
|
||||
}`,
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
.lifecycle { margin: 8px 0; padding: 10px 12px; border: 1px solid var(--app-border-control, #cbd5e1); border-radius: 8px; color: var(--app-text-control, #1e293b); background: var(--app-surface, #fff); font-size: 12px; }
|
||||
summary { cursor: pointer; font-weight: 600; }
|
||||
dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin: 12px 0; }
|
||||
dt, p, small { color: var(--app-text-muted, #64748b); }
|
||||
dd { margin: 4px 0 0; font-variant-numeric: tabular-nums; }
|
||||
p { margin: 8px 0; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
`]
|
||||
})
|
||||
export class TaskLifecycleComponent {
|
||||
value = input<TaskLifecycle | null>(null);
|
||||
duration = durationText;
|
||||
items = [
|
||||
{key:'total', label:'总历时'}, {key:'queued', label:'后台排队'},
|
||||
{key:'resource_wait', label:'等待设备/资源'}, {key:'paused', label:'暂停与示范'},
|
||||
{key:'execution', label:'执行与整理'}, {key:'final_review', label:'最终验收'},
|
||||
{key:'report_export', label:'报告导出'}
|
||||
];
|
||||
phase(value: string): string {
|
||||
return ({queued:'后台排队', waiting_resources:'等待设备/资源', running:'执行中', paused:'已暂停',
|
||||
demonstrating:'人工示范中', analyzing_demo:'正在分析示范', final_review:'正在最终验收',
|
||||
report_export:'正在导出报告', finished:'执行已结束', unknown:'状态未记录'} as Record<string,string>)[value] || '状态待确认';
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import { Session } from '../../core/models/session.model';
|
||||
template: `<span class="mode-badge" [class]="'mode-badge ' + mode()" [title]="'执行模式:' + label()"><span class="material-symbols-outlined" aria-hidden="true">{{icon()}}</span>{{label()}}</span>`,
|
||||
styles: [`
|
||||
:host{display:inline-flex;vertical-align:middle}
|
||||
.mode-badge{display:inline-flex;align-items:center;box-sizing:border-box;height:20px;gap:3px;padding:0 6px;border:1px solid #e2e8f0;border-radius:5px;background:#f8fafc;color:#475569;font-size:11px;font-weight:600;line-height:14px;white-space:nowrap}
|
||||
.mode-badge{display:inline-flex;align-items:center;box-sizing:border-box;height:20px;gap:3px;padding:0 6px;border:1px solid var(--app-border, #e2e8f0);border-radius:5px;background:var(--app-surface-soft, #f8fafc);color:var(--app-text-secondary, #475569);font-size:11px;font-weight:600;line-height:14px;white-space:nowrap}
|
||||
.material-symbols-outlined{font-size:13px;line-height:1}
|
||||
.explore{background:#eef2ff;border-color:#c7d2fe;color:#4338ca}
|
||||
.flash{background:#fff7ed;border-color:#fed7aa;color:#9a3412}
|
||||
.pro{background:#eff6ff;border-color:#bfdbfe;color:#1d4ed8}
|
||||
.explore{background:var(--app-primary-soft, #eef2ff);border-color:var(--app-primary-border, #c7d2fe);color:var(--app-exploration-text, #4338ca)}
|
||||
.flash{background:var(--app-warning-soft, #fff7ed);border-color:var(--app-warning-border, #fed7aa);color:var(--app-warning-text, #9a3412)}
|
||||
.pro{background:var(--app-primary-soft, #eff6ff);border-color:var(--app-primary-border, #bfdbfe);color:var(--app-primary-text, #1d4ed8)}
|
||||
`]
|
||||
})
|
||||
export class TaskModeBadgeComponent {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
:host { display: block; width: 100%; color: #475569; font-size: 12px; }
|
||||
.plugin-toggle { display: inline-flex; align-items: center; gap: 6px; border: 1px solid #e2e8f0; border-radius: 20px; padding: 5px 10px; background: #f8fafc; color: #64748b; font: inherit; cursor: pointer; transition: background .15s, border-color .15s; }
|
||||
.plugin-toggle:hover, .plugin-toggle[aria-expanded=true] { background: #eff6ff; border-color: #bfdbfe; color: #2563eb; }
|
||||
:host { display: block; width: 100%; color: var(--app-text-secondary, #475569); font-size: 12px; }
|
||||
.plugin-toggle { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--app-border, #e2e8f0); border-radius: 20px; padding: 5px 10px; background: var(--app-surface-soft, #f8fafc); color: var(--app-text-muted, #64748b); font: inherit; cursor: pointer; transition: background .15s, border-color .15s; }
|
||||
.plugin-toggle:hover, .plugin-toggle[aria-expanded=true] { background: var(--app-primary-soft, #eff6ff); border-color: var(--app-primary-border, #bfdbfe); color: var(--app-primary-text, #2563eb); }
|
||||
.material-symbols-outlined { font-size: 16px; }
|
||||
.mode { color: #2563eb; padding-left: 6px; border-left: 1px solid #dce5ef; }
|
||||
.mode { color: var(--app-primary-text, #2563eb); padding-left: 6px; border-left: 1px solid var(--app-primary-border, #dce5ef); }
|
||||
.chevron { font-size: 15px; }
|
||||
.plugin-options { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 14px; padding: 14px 16px; margin-top: 8px; display: grid; gap: 12px; }
|
||||
.plugin-options { background: var(--app-surface-soft, #f8fafc); border: 1px solid var(--app-border, #e2e8f0); border-radius: 14px; padding: 14px 16px; margin-top: 8px; display: grid; gap: 12px; }
|
||||
p { margin: 0; line-height: 1.65; }
|
||||
label { display: flex; align-items: center; gap: 8px; }
|
||||
.auto-option { font-weight: 500; color: #334155; }
|
||||
input[type=checkbox] { accent-color: #2563eb; margin: 0; width: 14px; height: 14px; flex-shrink: 0; }
|
||||
.auto-option { font-weight: 500; color: var(--app-text-control, #334155); }
|
||||
input[type=checkbox] { accent-color: var(--app-primary-text, #2563eb); margin: 0; width: 14px; height: 14px; flex-shrink: 0; }
|
||||
.choice { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
select { font: inherit; color: #334155; background: #fff; border: 1px solid #dce3eb; border-radius: 8px; padding: 6px 10px; max-width: 100%; }
|
||||
select { font: inherit; color: var(--app-text-control, #334155); background: var(--app-surface, #fff); border: 1px solid var(--app-primary-border, #dce3eb); border-radius: 8px; padding: 6px 10px; max-width: 100%; }
|
||||
select:disabled { opacity: .5; }
|
||||
.hint { color: #64748b; font-size: 11px; }
|
||||
.error { color: #b91c1c; }
|
||||
.text-button, .manage-link { color: #2563eb; font: inherit; text-decoration: none; }
|
||||
.hint { color: var(--app-text-muted, #64748b); font-size: 11px; }
|
||||
.error { color: var(--app-error-text, #b91c1c); }
|
||||
.text-button, .manage-link { color: var(--app-primary-text, #2563eb); font: inherit; text-decoration: none; }
|
||||
.text-button { background: none; border: 0; padding: 0 6px; cursor: pointer; }
|
||||
.manage-link { display: inline-flex; align-items: center; gap: 4px; justify-self: start; }
|
||||
:focus-visible { outline: 2px solid #3b82f6; outline-offset: 3px; }
|
||||
+3
-3
@@ -15,12 +15,12 @@ export function testResultSourceLabel(result: CaseTestResult | null | undefined)
|
||||
template: `<span [class]="'test-result-badge ' + status()" [class.compact]="compact()" [class.prominent]="prominent()" [title]="sourceLabel() + ':' + (result()?.reason || '执行状态与测试结论分别记录')"><span class="material-symbols-outlined" aria-hidden="true">{{icon()}}</span>{{label()}}</span>`,
|
||||
styles: [`
|
||||
:host{display:inline-flex;vertical-align:middle;max-width:100%}
|
||||
.test-result-badge{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;border-radius:6px;border:1px solid #cbd5e1;padding:3px 8px;font-size:12px;font-weight:700;line-height:1.5;background:#f1f5f9;color:#475569}
|
||||
.test-result-badge{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;border-radius:6px;border:1px solid var(--app-border-control, #cbd5e1);padding:3px 8px;font-size:12px;font-weight:700;line-height:1.5;background:var(--app-surface-soft, #f1f5f9);color:var(--app-text-secondary, #475569)}
|
||||
.material-symbols-outlined{font-size:16px;line-height:1}
|
||||
.passed{background:#15803d;color:#fff;border-color:#15803d}
|
||||
.failed{background:#b91c1c;color:#fff;border-color:#b91c1c}
|
||||
.inconclusive{background:#fff7ed;color:#9a3412;border-color:#fdba74}
|
||||
.blocked{background:#fef3c7;color:#92400e;border-color:#fbbf24}
|
||||
.inconclusive{background:var(--app-warning-soft, #fff7ed);color:var(--app-warning-text, #9a3412);border-color:var(--app-warning-border, #fdba74)}
|
||||
.blocked{background:var(--app-warning-soft, #fef3c7);color:var(--app-warning-text, #92400e);border-color:#fbbf24}
|
||||
.compact{box-sizing:border-box;height:20px;padding:0 6px;font-size:11px;line-height:14px;gap:3px;border-radius:5px}
|
||||
.compact .material-symbols-outlined{font-size:13px}
|
||||
.prominent{padding:4px 9px;font-size:14px;gap:5px}
|
||||
|
||||
+14
-14
@@ -29,24 +29,24 @@ import { TestResultBadgeComponent, testResultSourceLabel } from './test-result-b
|
||||
</section>`,
|
||||
styles: [`
|
||||
:host{display:block;margin:8px 0}
|
||||
.test-result-card{border:1px solid #cbd5e1;border-left:3px solid #64748b;border-radius:10px;padding:12px 16px;background:#f8fafc;color:#334155}
|
||||
.passed{border-color:#bbf7d0;border-left-color:#15803d;background:#f0fdf4}
|
||||
.failed{border-color:#fecaca;border-left-color:#b91c1c;background:#fef2f2}
|
||||
.inconclusive,.blocked{border-color:#fde68a;border-left-color:#b45309;background:#fffbeb}
|
||||
.test-result-card{border:1px solid var(--app-border-control, #cbd5e1);border-left:3px solid var(--app-border-control, #64748b);border-radius:10px;padding:12px 16px;background:var(--app-surface-soft, #f8fafc);color:var(--app-text-control, #334155)}
|
||||
.passed{border-color:var(--app-success-border, #bbf7d0);border-left-color:#15803d;background:var(--app-success-soft, #f0fdf4)}
|
||||
.failed{border-color:var(--app-error-border, #fecaca);border-left-color:#b91c1c;background:var(--app-error-soft, #fef2f2)}
|
||||
.inconclusive,.blocked{border-color:var(--app-warning-border, #fde68a);border-left-color:#b45309;background:var(--app-warning-soft, #fffbeb)}
|
||||
header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
|
||||
.caption{font-size:12px;font-weight:600;color:#475569}
|
||||
.caption small{font-size:11px;font-weight:400;margin-left:7px;color:#64748b}
|
||||
.reason{font-size:13px;line-height:1.7;margin:8px 0 0;white-space:pre-wrap;overflow-wrap:anywhere;color:#1e293b}
|
||||
details{margin-top:6px;font-size:12px;line-height:1.7;color:#475569}
|
||||
.caption{font-size:12px;font-weight:600;color:var(--app-text-secondary, #475569)}
|
||||
.caption small{font-size:11px;font-weight:400;margin-left:7px;color:var(--app-text-muted, #64748b)}
|
||||
.reason{font-size:13px;line-height:1.7;margin:8px 0 0;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--app-text-control, #1e293b)}
|
||||
details{margin-top:6px;font-size:12px;line-height:1.7;color:var(--app-text-secondary, #475569)}
|
||||
summary{cursor:pointer;width:fit-content;padding:2px 0;font-weight:600}
|
||||
ol{padding-left:22px;margin:8px 0 0}li{padding:3px 0;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
.review-time{margin:5px 0 0;color:#64748b;font-size:11px}
|
||||
.review-time{margin:5px 0 0;color:var(--app-text-muted, #64748b);font-size:11px}
|
||||
.review-toolbar,.review-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px;font-size:12px}
|
||||
button{border:1px solid #cbd5e1;border-radius:6px;padding:4px 9px;background:#fff;color:#475569;font:inherit;cursor:pointer;line-height:1.5}
|
||||
button:hover{border-color:#94a3b8;background:#f8fafc}button:disabled{opacity:.55;cursor:wait}button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}
|
||||
.review-toolbar span{color:#15803d}.review-form{border-top:1px solid #cbd5e1;margin-top:10px;padding-top:10px;font-size:12px}
|
||||
label{display:block;color:#475569}textarea{display:block;box-sizing:border-box;width:100%;resize:vertical;border:1px solid #cbd5e1;border-radius:6px;margin-top:5px;padding:7px 9px;color:#1e293b;background:#fff;font:inherit;line-height:1.6}
|
||||
.confirm-pass{color:#166534;border-color:#86efac;background:#f0fdf4}.confirm-fail{color:#991b1b;border-color:#fca5a5;background:#fef2f2}.review-error{color:#b91c1c;margin:8px 0 0}
|
||||
button{border:1px solid var(--app-border-control, #cbd5e1);border-radius:6px;padding:4px 9px;background:var(--app-surface, #fff);color:var(--app-text-secondary, #475569);font:inherit;cursor:pointer;line-height:1.5}
|
||||
button:hover{border-color:var(--app-border-control, #94a3b8);background:var(--app-surface-soft, #f8fafc)}button:disabled{opacity:.55;cursor:wait}button:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}
|
||||
.review-toolbar span{color:var(--app-success-text, #15803d)}.review-form{border-top:1px solid var(--app-border-control, #cbd5e1);margin-top:10px;padding-top:10px;font-size:12px}
|
||||
label{display:block;color:var(--app-text-secondary, #475569)}textarea{display:block;box-sizing:border-box;width:100%;resize:vertical;border:1px solid var(--app-border-control, #cbd5e1);border-radius:6px;margin-top:5px;padding:7px 9px;color:var(--app-text-control, #1e293b);background:var(--app-surface, #fff);font:inherit;line-height:1.6}
|
||||
.confirm-pass{color:var(--app-success-text, #166534);border-color:var(--app-success-border, #86efac);background:var(--app-success-soft, #f0fdf4)}.confirm-fail{color:var(--app-error-text, #991b1b);border-color:var(--app-error-border, #fca5a5);background:var(--app-error-soft, #fef2f2)}.review-error{color:var(--app-error-text, #b91c1c);margin:8px 0 0}
|
||||
@media(max-width:480px){.test-result-card{padding:10px 12px}}
|
||||
`]
|
||||
})
|
||||
|
||||
@@ -21,6 +21,15 @@ export interface ModelInfo {
|
||||
architecture?: string;
|
||||
}
|
||||
|
||||
export interface TaskLifecycle {
|
||||
schema_version: number;
|
||||
status: string;
|
||||
phase: string;
|
||||
observed_at: number | null;
|
||||
durations: Record<string, number | null>;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface CaseTestResult {
|
||||
status: 'passed' | 'failed' | 'inconclusive' | 'blocked';
|
||||
reason: string;
|
||||
@@ -39,6 +48,7 @@ export interface CaseReviewResponse {
|
||||
|
||||
export interface TaskQueueItem {
|
||||
session_id: string;
|
||||
phase?: string | null;
|
||||
batch_id?: string | null;
|
||||
goal: string;
|
||||
profile?: string;
|
||||
@@ -51,6 +61,7 @@ export interface TaskQueueItem {
|
||||
|
||||
export interface Session {
|
||||
session_id: string;
|
||||
phase?: string | null;
|
||||
profile?: string | null;
|
||||
exploration_id?: string;
|
||||
exploration_latest_run_id?: string;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
$bg-base: #e2e5ea;
|
||||
$glass-bg: rgba(255, 255, 255, 0.45);
|
||||
$glass-border: rgba(255, 255, 255, 0.6);
|
||||
$glass-bg: rgb(var(--app-surface-rgb, 255 255 255) / 0.45);
|
||||
$glass-border: rgb(var(--app-border-rgb, 255 255 255) / 0.6);
|
||||
|
||||
.workspace-container {
|
||||
position: relative;
|
||||
@@ -40,7 +40,7 @@ $glass-border: rgba(255, 255, 255, 0.6);
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #e2e5ea 0%, #cfd6df 100%);
|
||||
background: linear-gradient(135deg, var(--app-primary-soft, #e2e5ea) 0%, var(--app-primary-soft, #cfd6df) 100%);
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
@@ -94,8 +94,8 @@ $glass-border: rgba(255, 255, 255, 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.8);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.35);
|
||||
border-right: 1px solid rgb(var(--app-border-rgb, 255 255 255) / 0.8);
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
@@ -106,7 +106,7 @@ $glass-border: rgba(255, 255, 255, 0.6);
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: #ffffff;
|
||||
background-color: var(--app-surface, #ffffff);
|
||||
box-shadow: -6px 0 24px rgba(0, 0, 0, 0.03);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -126,7 +126,7 @@ $glass-border: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 -6px;
|
||||
|
||||
&:hover, .workspace-container.is-resizing & {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgb(var(--app-surface-rgb, 255 255 255) / 0.15);
|
||||
|
||||
.resizer-handle {
|
||||
background: rgba(94, 114, 228, 0.8);
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
:host { display: block; color: #1e293b; font-size: 14px; }
|
||||
:host { display: block; color: var(--app-text-control, #1e293b); font-size: 14px; }
|
||||
.plugins-page { max-width: 1120px; padding: 88px 32px 56px; margin: auto; }
|
||||
header, .card-top, footer, .inline, .install-progress > div { display: flex; align-items: center; gap: 12px; }
|
||||
header { justify-content: space-between; margin-bottom: 28px; }
|
||||
h1 { margin: 8px 0; font-size: 30px; letter-spacing: -.6px; } h2 { margin: 0 0 6px; font-size: 17px; }
|
||||
p { color: #64748b; line-height: 1.7; white-space: pre-wrap; margin: 8px 0 16px; }
|
||||
.eyebrow { color: #3b6e9c; font-size: 11px; letter-spacing: 1.4px; font-weight: 600; }
|
||||
button { border: 1px solid #dce3eb; border-radius: 20px; background: #fff; color: #334155; padding: 8px 16px; font-size: 13px; cursor: pointer; white-space: nowrap; }
|
||||
button:hover:not(:disabled) { border-color: #9fb7d3; background: #f0f6ff; } button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
button.primary { color: #fff; background: #2563eb; border-color: #2563eb; } button.danger { color: #b91c1c; border-color: #fecaca; }
|
||||
.text-button { border: 0; margin-left: auto; color: #64748b; } :focus-visible { outline: 2px solid #3b82f6; outline-offset: 3px; }
|
||||
.tabs { display: flex; gap: 4px; width: fit-content; margin-bottom: 24px; background: #f1f5f9; padding: 4px; border-radius: 24px; }
|
||||
.tabs button { border: 0; background: transparent; }.tabs button.active { color: #1d4ed8; background: #fff; box-shadow: 0 1px 4px #0f172a0d; font-weight: 600; }.tabs span { margin-left: 8px; }
|
||||
p { color: var(--app-text-muted, #64748b); line-height: 1.7; white-space: pre-wrap; margin: 8px 0 16px; }
|
||||
.eyebrow { color: var(--app-primary-text, #3b6e9c); font-size: 11px; letter-spacing: 1.4px; font-weight: 600; }
|
||||
button { border: 1px solid var(--app-primary-border, #dce3eb); border-radius: 20px; background: var(--app-surface, #fff); color: var(--app-text-control, #334155); padding: 8px 16px; font-size: 13px; cursor: pointer; white-space: nowrap; }
|
||||
button:hover:not(:disabled) { border-color: var(--app-primary-border, #9fb7d3); background: var(--app-primary-soft, #f0f6ff); } button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
button.primary { color: #fff; background: #2563eb; border-color: #2563eb; } button.danger { color: var(--app-error-text, #b91c1c); border-color: var(--app-error-border, #fecaca); }
|
||||
.text-button { border: 0; margin-left: auto; color: var(--app-text-muted, #64748b); } :focus-visible { outline: 2px solid #3b82f6; outline-offset: 3px; }
|
||||
.tabs { display: flex; gap: 4px; width: fit-content; margin-bottom: 24px; background: var(--app-surface-soft, #f1f5f9); padding: 4px; border-radius: 24px; }
|
||||
.tabs button { border: 0; background: transparent; }.tabs button.active { color: var(--app-primary-text, #1d4ed8); background: var(--app-surface, #fff); box-shadow: 0 1px 4px #0f172a0d; font-weight: 600; }.tabs span { margin-left: 8px; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 18px; }
|
||||
.card { padding: 22px; background: #fff; border: 1px solid #e1e7ef; border-radius: 14px; }.card > p { min-height: 48px; margin-top: 18px; }.card footer { margin-top: 20px; flex-wrap: wrap; }
|
||||
.plugin-icon { display: grid; place-items: center; width: 44px; height: 44px; flex-shrink: 0; color: #4477ba; background: #eef4ff; border-radius: 12px; }
|
||||
.meta, small { color: #64748b; font-size: 12px; line-height: 1.6; }.badge { margin-left: auto; white-space: nowrap; padding: 4px 9px; border-radius: 20px; background: #f1f5f9; font-size: 11px; color: #64748b; }.badge.enabled { background: #ecfdf5; color: #047857; }
|
||||
.empty { padding: 55px 24px; text-align: center; border: 1px dashed #cbd5e1; border-radius: 14px; background: #ffffff80; margin-bottom: 24px; }.empty > span { font-size: 36px; color: #94a3b8; margin-bottom: 16px; }
|
||||
.message { padding: 12px 16px; border-radius: 8px; margin-bottom: 18px; line-height: 1.6; }.error { color: #b91c1c; background: #fff1f2; }.success { color: #047857; background: #ecfdf5; }
|
||||
.install-progress { background: #fff; border: 1px solid #dbeafe; padding: 18px; border-radius: 12px; margin-bottom: 20px; }.install-progress > div { justify-content: space-between; }progress { width: 100%; height: 8px; margin-top: 12px; accent-color: #2563eb; }
|
||||
details { margin: 16px 0; }summary { cursor: pointer; color: #475569; padding: 8px 0; }.advanced { padding: 18px 22px; border: 1px solid #e2e8f0; border-radius: 12px; margin-top: 24px; }
|
||||
label { display: flex; flex-direction: column; gap: 7px; margin: 16px 0; font-size: 13px; }input, select, textarea { border: 1px solid #d5dde7; background: #fff; border-radius: 8px; padding: 10px 12px; color: #1e293b; font-size: 13px; min-width: 0; width: 100%; }textarea { resize: vertical; font-family: ui-monospace, 'Microsoft YaHei', monospace; line-height: 1.6; }.inline > input, .inline > select { flex: 1; }
|
||||
.modal-backdrop { position: fixed; inset: 0; background: #0f172a50; backdrop-filter: blur(3px); display: grid; place-items: center; z-index: 1300; padding: 24px; }.dialog { max-width: 620px; width: 100%; max-height: 90vh; overflow-y: auto; background: #fff; padding: 28px; border-radius: 16px; box-shadow: 0 24px 80px #0f172a30; }.dialog.small { max-width: 430px; }.dialog header { margin-bottom: 12px; }.dialog footer { justify-content: flex-end; margin-top: 24px; }.two-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }.checkbox { flex-direction: row; align-items: center; gap: 10px; }.checkbox input { width: auto; }
|
||||
.card { padding: 22px; background: var(--app-surface, #fff); border: 1px solid var(--app-primary-border, #e1e7ef); border-radius: 14px; }.card > p { min-height: 48px; margin-top: 18px; }.card footer { margin-top: 20px; flex-wrap: wrap; }
|
||||
.plugin-icon { display: grid; place-items: center; width: 44px; height: 44px; flex-shrink: 0; color: var(--app-primary-text, #4477ba); background: var(--app-primary-soft, #eef4ff); border-radius: 12px; }
|
||||
.meta, small { color: var(--app-text-muted, #64748b); font-size: 12px; line-height: 1.6; }.badge { margin-left: auto; white-space: nowrap; padding: 4px 9px; border-radius: 20px; background: var(--app-surface-soft, #f1f5f9); font-size: 11px; color: var(--app-text-muted, #64748b); }.badge.enabled { background: var(--app-success-soft, #ecfdf5); color: var(--app-success-text, #047857); }
|
||||
.empty { padding: 55px 24px; text-align: center; border: 1px dashed var(--app-border-control, #cbd5e1); border-radius: 14px; background: rgb(var(--app-surface-rgb, 255 255 255) / 0.5020); margin-bottom: 24px; }.empty > span { font-size: 36px; color: var(--app-text-faint, #94a3b8); margin-bottom: 16px; }
|
||||
.message { padding: 12px 16px; border-radius: 8px; margin-bottom: 18px; line-height: 1.6; }.error { color: var(--app-error-text, #b91c1c); background: var(--app-error-soft, #fff1f2); }.success { color: var(--app-success-text, #047857); background: var(--app-success-soft, #ecfdf5); }
|
||||
.install-progress { background: var(--app-surface, #fff); border: 1px solid var(--app-primary-border, #dbeafe); padding: 18px; border-radius: 12px; margin-bottom: 20px; }.install-progress > div { justify-content: space-between; }progress { width: 100%; height: 8px; margin-top: 12px; accent-color: var(--app-primary-text, #2563eb); }
|
||||
details { margin: 16px 0; }summary { cursor: pointer; color: var(--app-text-secondary, #475569); padding: 8px 0; }.advanced { padding: 18px 22px; border: 1px solid var(--app-border, #e2e8f0); border-radius: 12px; margin-top: 24px; }
|
||||
label { display: flex; flex-direction: column; gap: 7px; margin: 16px 0; font-size: 13px; }input, select, textarea { border: 1px solid var(--app-primary-border, #d5dde7); background: var(--app-surface, #fff); border-radius: 8px; padding: 10px 12px; color: var(--app-text-control, #1e293b); font-size: 13px; min-width: 0; width: 100%; }textarea { resize: vertical; font-family: ui-monospace, 'Microsoft YaHei', monospace; line-height: 1.6; }.inline > input, .inline > select { flex: 1; }
|
||||
.modal-backdrop { position: fixed; inset: 0; background: #0f172a50; backdrop-filter: blur(3px); display: grid; place-items: center; z-index: 1300; padding: 24px; }.dialog { max-width: 620px; width: 100%; max-height: 90vh; overflow-y: auto; background: var(--app-surface, #fff); padding: 28px; border-radius: 16px; box-shadow: 0 24px 80px #0f172a30; }.dialog.small { max-width: 430px; }.dialog header { margin-bottom: 12px; }.dialog footer { justify-content: flex-end; margin-top: 24px; }.two-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }.checkbox { flex-direction: row; align-items: center; gap: 10px; }.checkbox input { width: auto; }
|
||||
@media (max-width: 640px) { .plugins-page { padding: 80px 16px 32px; }.card { padding: 18px; }.dialog { padding: 20px; }.two-columns { grid-template-columns: 1fr; gap: 0; }.card-top { gap: 8px; }h2 { font-size: 15px; } }
|
||||
|
||||
.loading-icon { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .loading-icon { animation: none; } }
|
||||
|
||||
.badge.preview { background: #fffbeb; color: #92400e; }
|
||||
.badge.preview { background: var(--app-warning-soft, #fffbeb); color: var(--app-warning-text, #92400e); }
|
||||
@@ -1,16 +1,16 @@
|
||||
:host {display:block;height:100vh;width:100%;overflow:hidden;color:#334155}
|
||||
.workspace-container {display:flex;height:100%;background:#f8fafc;overflow:hidden}
|
||||
.left-panel {flex:1;min-width:420px;display:flex;flex-direction:column;overflow:hidden;background:#f8fafc}
|
||||
:host {display:block;height:100vh;width:100%;overflow:hidden;color:var(--app-text-control, #334155)}
|
||||
.workspace-container {display:flex;height:100%;background:var(--app-surface-soft, #f8fafc);overflow:hidden}
|
||||
.left-panel {flex:1;min-width:420px;display:flex;flex-direction:column;overflow:hidden;background:var(--app-surface-soft, #f8fafc)}
|
||||
app-agent-stream,app-exploration-panel {flex:1;min-height:0}
|
||||
.right-panel {min-width:280px;max-width:50%;display:flex;flex-direction:column;background:white;border-left:1px solid #e2e8f0}
|
||||
.right-panel {min-width:280px;max-width:50%;display:flex;flex-direction:column;background:var(--app-surface, white);border-left:1px solid var(--app-border, #e2e8f0)}
|
||||
.resizer {width:8px;margin:0 -4px;cursor:col-resize;z-index:5;display:flex;align-items:center;justify-content:center}
|
||||
.resizer-handle {width:3px;height:32px;border-radius:4px;background:#cbd5e1}
|
||||
.resizer-handle {width:3px;height:32px;border-radius:4px;background:var(--app-surface-raised, #cbd5e1)}
|
||||
.is-resizing {cursor:col-resize;user-select:none}
|
||||
.task-composer {flex:0 0 auto;margin:0 20px 16px;padding:12px 16px;background:#ffffffec;border:1px solid #dce5ee;border-radius:16px;box-shadow:0 6px 24px #0f172a08;font-size:13px}
|
||||
.composer-heading {display:flex;gap:12px;align-items:center;margin-bottom:8px}.composer-heading strong{white-space:nowrap}.composer-heading span{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:#64748b;font-size:12px}
|
||||
textarea {box-sizing:border-box;width:100%;border:0;background:transparent;resize:vertical;min-height:44px;max-height:140px;line-height:1.6;color:#0f172a;font:inherit;outline:none}
|
||||
textarea:focus-visible{box-shadow:0 2px #93c5fd}textarea:disabled{color:#94a3b8}
|
||||
.composer-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}.composer-actions small{flex:1;color:#64748b;min-width:180px}
|
||||
button{font:inherit;border:1px solid #dbe3ec;border-radius:8px;padding:6px 12px;background:white;color:#475569;cursor:pointer}button.primary{background:#0f172a;color:white;border-color:#0f172a}button:disabled{opacity:.45;cursor:default}
|
||||
.feedback{font-size:12px;margin:0 0 8px;color:#047857}.error{color:#b91c1c}.origin-note{margin:12px 0;padding:10px 12px;border:1px solid #dbe3ec;border-radius:10px;font-size:12px}.origin-note button{margin-left:10px}
|
||||
.task-composer {flex:0 0 auto;margin:0 20px 16px;padding:12px 16px;background:rgb(var(--app-surface-rgb, 255 255 255) / 0.9255);border:1px solid var(--app-border, #dce5ee);border-radius:16px;box-shadow:0 6px 24px #0f172a08;font-size:13px}
|
||||
.composer-heading {display:flex;gap:12px;align-items:center;margin-bottom:8px}.composer-heading strong{white-space:nowrap}.composer-heading span{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--app-text-muted, #64748b);font-size:12px}
|
||||
textarea {box-sizing:border-box;width:100%;border:0;background:transparent;resize:vertical;min-height:44px;max-height:140px;line-height:1.6;color:var(--app-text-strong, #0f172a);font:inherit;outline:none}
|
||||
textarea:focus-visible{box-shadow:0 2px #93c5fd}textarea:disabled{color:var(--app-text-faint, #94a3b8)}
|
||||
.composer-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}.composer-actions small{flex:1;color:var(--app-text-muted, #64748b);min-width:180px}
|
||||
button{font:inherit;border:1px solid var(--app-border, #dbe3ec);border-radius:8px;padding:6px 12px;background:var(--app-surface, white);color:var(--app-text-secondary, #475569);cursor:pointer}button.primary{background:#0f172a;color:white;border-color:var(--app-border-control, #0f172a)}button:disabled{opacity:.45;cursor:default}
|
||||
.feedback{font-size:12px;margin:0 0 8px;color:var(--app-success-text, #047857)}.error{color:var(--app-error-text, #b91c1c)}.origin-note{margin:12px 0;padding:10px 12px;border:1px solid var(--app-border, #dbe3ec);border-radius:10px;font-size:12px}.origin-note button{margin-left:10px}
|
||||
@media(max-width:800px){.left-panel{min-width:320px}.right-panel{min-width:240px}.task-composer{margin:0 10px 10px;padding:10px}}
|
||||
@@ -104,7 +104,7 @@ describe('AgentService browser connection lifecycle', () => {
|
||||
}
|
||||
const status = http.expectOne('/api/status');
|
||||
const sessions = http.expectOne('/api/sessions');
|
||||
const guidance = http.expectOne('/api/tasks/selected-task/guidance');
|
||||
const guidance = http.expectOne('/api/tasks/selected-task/guidance?details=true');
|
||||
tick(UI_READ_TIMEOUT_MS);
|
||||
expect(status.cancelled).toBeTrue();
|
||||
expect(sessions.cancelled).toBeTrue();
|
||||
@@ -113,8 +113,41 @@ describe('AgentService browser connection lifecycle', () => {
|
||||
service.fetchStatus();
|
||||
service.fetchSessions();
|
||||
service.fetchGuidance('selected-task');
|
||||
http.expectOne('/api/tasks/selected-task/guidance').flush([]);
|
||||
http.expectOne('/api/tasks/selected-task/guidance?details=true').flush([]);
|
||||
flushSnapshots();
|
||||
service.destroy();
|
||||
}));
|
||||
|
||||
it('refreshes lifecycle with unchanged receipts and accepts the older guidance array', () => {
|
||||
service = TestBed.inject(AgentService);
|
||||
flushSnapshots();
|
||||
service.currentSessionId.set('progress-task');
|
||||
const instructions = [{id:'g1', instruction:'检查现有证据', status:'unapplied', timestamp:10, accepted_at:10}];
|
||||
const lifecycle = {schema_version:1, status:'completed', phase:'finished', observed_at:30, durations:{execution:20}};
|
||||
service.fetchGuidance('progress-task');
|
||||
http.expectOne('/api/tasks/progress-task/guidance?details=true').flush({instructions, lifecycle});
|
||||
expect(service.guidanceEvents()[0].status).toBe('unapplied');
|
||||
expect(service.currentLifecycle()?.durations['execution']).toBe(20);
|
||||
service.fetchGuidance('progress-task');
|
||||
http.expectOne('/api/tasks/progress-task/guidance?details=true').flush({instructions, lifecycle:{...lifecycle, durations:{execution:25}}});
|
||||
expect(service.currentLifecycle()?.durations['execution']).toBe(25);
|
||||
service.fetchGuidance('progress-task');
|
||||
http.expectOne('/api/tasks/progress-task/guidance?details=true').flush(instructions);
|
||||
expect(service.currentLifecycle()).toBeNull();
|
||||
expect(service.guidanceEvents().length).toBe(1);
|
||||
});
|
||||
|
||||
it('ignores lifecycle and guidance returned after selecting another task', () => {
|
||||
service = TestBed.inject(AgentService);
|
||||
flushSnapshots();
|
||||
service.currentSessionId.set('old-task');
|
||||
service.fetchGuidance('old-task');
|
||||
service.currentSessionId.set('new-task');
|
||||
http.expectOne('/api/tasks/old-task/guidance?details=true').flush({
|
||||
instructions:[{id:'old', status:'consumed'}],
|
||||
lifecycle:{schema_version:1, status:'running', phase:'running', observed_at:30, durations:{execution:20}}
|
||||
});
|
||||
expect(service.currentLifecycle()).toBeNull();
|
||||
expect(service.guidanceEvents()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import { Observable, finalize, tap, timeout } from 'rxjs';
|
||||
import { UI_READ_TIMEOUT_MS } from '../core/http-timeouts';
|
||||
import { SharedLiveStream } from './shared-live-stream';
|
||||
|
||||
import { Session, ModelInfo, TaskQueueItem, AgentStatusResponse, SessionUsage, CaseReviewResponse } from '../core/models/session.model';
|
||||
import { Session, ModelInfo, TaskQueueItem, AgentStatusResponse, SessionUsage, CaseReviewResponse, TaskLifecycle } from '../core/models/session.model';
|
||||
import { ProTuningDefaults, ProTuningOptions } from '../core/models/pro-tuning.model';
|
||||
import { StepItemData, StepReplayFrame, LLMStreamResetEventData, StreamResetNotice, DEFAULT_STREAM_RESET_MESSAGE, PersistedCheckerStream, StreamSegment } from '../core/models/stream.model';
|
||||
import { extractStepReplayFrames } from '../utils/action-formatter.util';
|
||||
@@ -73,7 +73,8 @@ export interface GuidanceEvent {
|
||||
instruction: string;
|
||||
source: string;
|
||||
timestamp: number;
|
||||
status: 'pending' | 'consumed';
|
||||
status: 'pending' | 'reading' | 'consumed' | 'unapplied' | 'unconfirmed';
|
||||
accepted_at?: number;
|
||||
consumed_at?: number;
|
||||
}
|
||||
|
||||
@@ -191,6 +192,7 @@ export class AgentService {
|
||||
}
|
||||
const finalSession: Session = {
|
||||
...s,
|
||||
phase: isTerminal ? null : (activeMatch?.phase || pendingMatch?.phase || null),
|
||||
batch_id: s.batch_id || activeMatch?.batch_id || pendingMatch?.batch_id || null,
|
||||
status: sStatus,
|
||||
device_serial: serial,
|
||||
@@ -230,6 +232,7 @@ export class AgentService {
|
||||
const newSession: Session = {
|
||||
...this.explorationSessionFields(at),
|
||||
session_id: sid,
|
||||
phase: at.phase || null,
|
||||
batch_id: at.batch_id || null,
|
||||
display_name: at.display_name,
|
||||
initial_goal: at.goal || goal || '',
|
||||
@@ -278,6 +281,7 @@ export class AgentService {
|
||||
public currentSessionId = signal<string | null>(null);
|
||||
public sessionLogs = signal<any[]>([]); // Dynamic array of all raw events received
|
||||
public guidanceEvents = signal<GuidanceEvent[]>([]);
|
||||
public currentLifecycle = signal<TaskLifecycle | null>(null);
|
||||
private guidanceSignature = '';
|
||||
public isSessionContentLoading = signal<boolean>(false);
|
||||
public agentStatus = signal<string>('idle'); // Status of the agent runner process
|
||||
@@ -838,6 +842,7 @@ export class AgentService {
|
||||
this.currentSessionId.set(null);
|
||||
this.sessionLogs.set([]);
|
||||
this.guidanceEvents.set([]);
|
||||
this.currentLifecycle.set(null);
|
||||
this.guidanceSignature = '';
|
||||
this.currentNotes.set({});
|
||||
return;
|
||||
@@ -855,6 +860,7 @@ export class AgentService {
|
||||
this.currentSessionId.set(sessionId);
|
||||
this.sessionLogs.set([]); // Reset logs for new session selection
|
||||
this.guidanceEvents.set([]);
|
||||
this.currentLifecycle.set(null);
|
||||
this.guidanceSignature = '';
|
||||
this.fetchGuidance(sessionId);
|
||||
this.activePauseCardKey = null;
|
||||
@@ -1408,12 +1414,14 @@ export class AgentService {
|
||||
public fetchGuidance(sessionId: string): void {
|
||||
if (this.destroyed || this.currentSessionId() !== sessionId || this.guidanceRequestsPending.has(sessionId)) return;
|
||||
this.guidanceRequestsPending.add(sessionId);
|
||||
this.http.get<GuidanceEvent[]>(`/api/tasks/${encodeURIComponent(sessionId)}/guidance`).pipe(
|
||||
this.http.get<GuidanceEvent[] | {instructions: GuidanceEvent[]; lifecycle: TaskLifecycle}>(`/api/tasks/${encodeURIComponent(sessionId)}/guidance?details=true`).pipe(
|
||||
timeout(UI_READ_TIMEOUT_MS),
|
||||
finalize(() => this.guidanceRequestsPending.delete(sessionId))
|
||||
).subscribe({
|
||||
next: (items) => {
|
||||
next: (data) => {
|
||||
if (this.currentSessionId() !== sessionId) return;
|
||||
const items = Array.isArray(data) ? data : data.instructions;
|
||||
this.currentLifecycle.set(Array.isArray(data) ? null : data.lifecycle);
|
||||
const signature = JSON.stringify(items);
|
||||
if (signature !== this.guidanceSignature) {
|
||||
this.guidanceSignature = signature;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { THEME_STORAGE_KEY, ThemeService } from './theme.service';
|
||||
|
||||
describe('外观主题', () => {
|
||||
let stored: string | null;
|
||||
let systemDark: boolean;
|
||||
let media: EventTarget;
|
||||
let originalTheme: string | undefined;
|
||||
let originalScheme: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalTheme = document.documentElement.dataset['theme'];
|
||||
originalScheme = document.documentElement.style.colorScheme;
|
||||
stored = null;
|
||||
systemDark = false;
|
||||
media = new EventTarget();
|
||||
Object.defineProperty(media, 'matches', { get: () => systemDark });
|
||||
spyOn(window, 'matchMedia').and.returnValue(media as MediaQueryList);
|
||||
spyOn(Storage.prototype, 'getItem').and.callFake(key => key === THEME_STORAGE_KEY ? stored : null);
|
||||
spyOn(Storage.prototype, 'setItem').and.callFake((key, value) => { if (key === THEME_STORAGE_KEY) stored = value; });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
if (originalTheme) document.documentElement.dataset['theme'] = originalTheme;
|
||||
else delete document.documentElement.dataset['theme'];
|
||||
document.documentElement.style.colorScheme = originalScheme;
|
||||
});
|
||||
|
||||
it('follows system changes until the user chooses an explicit theme', () => {
|
||||
const theme = TestBed.inject(ThemeService);
|
||||
expect(document.documentElement.dataset['theme']).toBe('light');
|
||||
systemDark = true;
|
||||
media.dispatchEvent(new Event('change'));
|
||||
expect(document.documentElement.dataset['theme']).toBe('dark');
|
||||
theme.toggle();
|
||||
expect(stored).toBe('light');
|
||||
media.dispatchEvent(new Event('change'));
|
||||
expect(document.documentElement.dataset['theme']).toBe('light');
|
||||
theme.toggle();
|
||||
expect(document.documentElement.dataset['theme']).toBe('dark');
|
||||
theme.setPreference('system');
|
||||
expect(document.documentElement.dataset['theme']).toBe('dark');
|
||||
});
|
||||
|
||||
it('remembers dark mode after reloading and accepts changes from another tab', () => {
|
||||
TestBed.inject(ThemeService).setPreference('dark');
|
||||
TestBed.resetTestingModule();
|
||||
const reloaded = TestBed.inject(ThemeService);
|
||||
expect(reloaded.preference()).toBe('dark');
|
||||
expect(document.documentElement.style.colorScheme).toBe('dark');
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: THEME_STORAGE_KEY, newValue: 'light' }));
|
||||
expect(reloaded.preference()).toBe('light');
|
||||
expect(document.documentElement.dataset['theme']).toBe('light');
|
||||
});
|
||||
|
||||
it('still switches when browser storage is blocked', () => {
|
||||
(Storage.prototype.getItem as jasmine.Spy).and.throwError('blocked');
|
||||
(Storage.prototype.setItem as jasmine.Spy).and.throwError('blocked');
|
||||
const theme = TestBed.inject(ThemeService);
|
||||
theme.toggle();
|
||||
expect(document.documentElement.dataset['theme']).toBe('dark');
|
||||
expect(theme.preference()).toBe('dark');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { DestroyRef, Injectable, inject, signal } from '@angular/core';
|
||||
|
||||
export type ThemePreference = 'system' | 'light' | 'dark';
|
||||
export const THEME_STORAGE_KEY = 'artemis.theme';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ThemeService {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly window = this.document.defaultView;
|
||||
private readonly media = this.window?.matchMedia('(prefers-color-scheme: dark)');
|
||||
readonly preference = signal<ThemePreference>('system');
|
||||
readonly isDark = signal(false);
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
const saved = this.window?.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (saved === 'light' || saved === 'dark') this.preference.set(saved);
|
||||
} catch { /* A blocked store still allows switching in this tab. */ }
|
||||
const onSystemChange = () => this.apply();
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== THEME_STORAGE_KEY && event.key !== null) return;
|
||||
const value = event.newValue;
|
||||
this.preference.set(value === 'light' || value === 'dark' ? value : 'system');
|
||||
this.apply();
|
||||
};
|
||||
this.media?.addEventListener('change', onSystemChange);
|
||||
this.window?.addEventListener('storage', onStorage);
|
||||
inject(DestroyRef).onDestroy(() => {
|
||||
this.media?.removeEventListener('change', onSystemChange);
|
||||
this.window?.removeEventListener('storage', onStorage);
|
||||
});
|
||||
this.apply();
|
||||
}
|
||||
|
||||
setPreference(value: ThemePreference): void {
|
||||
if (!['system', 'light', 'dark'].includes(value)) return;
|
||||
this.preference.set(value);
|
||||
try { this.window?.localStorage.setItem(THEME_STORAGE_KEY, value); } catch { /* Tab-only preference. */ }
|
||||
this.apply();
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.setPreference(this.isDark() ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
private apply(): void {
|
||||
const dark = this.preference() === 'dark' || (this.preference() === 'system' && this.media?.matches === true);
|
||||
this.isDark.set(dark);
|
||||
this.document.documentElement.dataset['theme'] = dark ? 'dark' : 'light';
|
||||
this.document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,55 @@ describe('markdown-parser.util verification & check lines', () => {
|
||||
it('renders Chinese report tables and preserves separate lines safely', () => {
|
||||
const html = renderMarkdownToHtml('## 结论\n第一行\n第二行\n\n| 分类 | 结果 |\n| --- | --- |\n| 网络 | **通过** |');
|
||||
expect(html).toContain('<h2>结论</h2>');
|
||||
expect(html).toContain('<div>第一行</div>\n<div>第二行</div>');
|
||||
expect(html).toContain('<p>第一行<br>第二行</p>');
|
||||
expect(html).toContain('<th>分类</th>');
|
||||
expect(html).toContain('<td><strong>通过</strong></td>');
|
||||
});
|
||||
|
||||
it('keeps table alignment, escaped pipes and inline code intact', () => {
|
||||
const view = document.createElement('div');
|
||||
view.innerHTML = renderMarkdownToHtml('| 项目 | 值 |\n| :--- | ---: |\n| A\\|B | `**原样**` |');
|
||||
expect(view.querySelectorAll('th').length).toBe(2);
|
||||
expect(view.querySelector('td')?.textContent).toBe('A|B');
|
||||
expect(view.querySelector('td.md-align-right code')?.textContent).toBe('**原样**');
|
||||
expect(view.querySelector('code strong')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nested lists, task states, quotes and reference links', () => {
|
||||
const view = document.createElement('div');
|
||||
view.innerHTML = renderMarkdownToHtml('#### 计划\n- [x] 完成\n - 子项\n- [/] 执行中\n- [ ] 待办\n- assert@end: **最终断言**\n\n> 引用\n>\n> 3. [文档][docs]\n\n[docs]: https://example.com/a?q=1&x=2');
|
||||
expect(view.querySelector('h4')?.textContent).toBe('计划');
|
||||
expect(view.querySelector('li ul li')?.textContent).toBe('子项');
|
||||
expect(view.querySelectorAll('.md-task-marker').length).toBe(3);
|
||||
expect(view.querySelector('.md-task-item.progress')?.textContent).toContain('执行中');
|
||||
expect(view.querySelector('.verify-badge.assert')?.textContent).toBe('assert@end');
|
||||
expect(view.querySelector('blockquote ol')?.getAttribute('start')).toBe('3');
|
||||
expect(view.querySelector('a')?.getAttribute('href')).toBe('https://example.com/a?q=1&x=2');
|
||||
});
|
||||
|
||||
it('preserves code whitespace, markup and unfinished streaming fences', () => {
|
||||
const view = document.createElement('div');
|
||||
view.innerHTML = renderMarkdownToHtml('~~~xml\r\n<plan>**原样**</plan>\r\n\r\n\r\n code\r\n');
|
||||
expect(view.querySelector('pre code')?.textContent).toBe('<plan>**原样**</plan>\n\n\n code');
|
||||
expect(view.querySelector('plan,strong')).toBeNull();
|
||||
});
|
||||
|
||||
it('escapes raw and inline HTML and rejects active link and image protocols', () => {
|
||||
const view = document.createElement('div');
|
||||
view.innerHTML = renderMarkdownToHtml('<script>alert(1)</script>\n\n`<img src=x onerror=alert(1)>`\n\n[危险](javascript:alert%281%29) \n\n[相对链接](/reports/test)');
|
||||
expect(view.querySelector('script,img')).toBeNull();
|
||||
expect(view.querySelector('code')?.textContent).toBe('<img src=x onerror=alert(1)>');
|
||||
expect(view.querySelectorAll('a').length).toBe(1);
|
||||
expect(view.querySelector('a')?.getAttribute('href')).toBe('/reports/test');
|
||||
expect(view.querySelector('a')?.getAttribute('rel')).toBe('noopener noreferrer');
|
||||
});
|
||||
|
||||
it('renders Markdown inside internal wrappers while retaining those tags inside code', () => {
|
||||
const view = document.createElement('div');
|
||||
view.innerHTML = renderMarkdownToHtml('<thought>\n## 阶段\n\n- **核对**\n\n```xml\n<plan>字面内容</plan>\n```\n</thought>');
|
||||
expect(view.querySelector('h2')?.textContent).toBe('阶段');
|
||||
expect(view.querySelector('li strong')?.textContent).toBe('核对');
|
||||
expect(view.querySelector('pre code')?.textContent).toBe('<plan>字面内容</plan>');
|
||||
expect(view.querySelector('thought,plan')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote } from '../core/models/markdown.model';
|
||||
import { CheckerResult } from '../core/models/stream.model';
|
||||
import { Marked, Renderer } from 'marked';
|
||||
|
||||
/**
|
||||
* Split text into bold, code, and plain segments
|
||||
@@ -208,196 +209,89 @@ export function extractCheckerResult(text: string): CheckerResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format inline markdown segments (bold, italic, code, strikethrough)
|
||||
*/
|
||||
function formatInlineMarkdown(text: string): string {
|
||||
if (!text) return '';
|
||||
return text
|
||||
// Inline code first (so asterisks inside code aren't formatted)
|
||||
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||
// Bold + Italic
|
||||
.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>')
|
||||
.replace(/___([^_]+)___/g, '<strong><em>$1</em></strong>')
|
||||
// Bold
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/__([^_]+)__/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
.replace(/\b_([^_]+)_\b/g, '<em>$1</em>')
|
||||
// Strikethrough
|
||||
.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert basic markdown text to safe HTML for agent logs/thinking
|
||||
*/
|
||||
function safeMarkdownUrl(href: string, image = false): boolean {
|
||||
try {
|
||||
const url = new URL(href, 'https://artemis.invalid/');
|
||||
return ['http:', 'https:', ...(image ? [] : ['mailto:'])].includes(url.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const baseRenderer = new Renderer();
|
||||
const agentTag = /^<\/?(?:thought|short_term_memory|strong_term_memory|reasoning|plan|task_plan|call_tool)\b[^\n>]*>/i;
|
||||
function agentTagToken(src: string) {
|
||||
const match = src.match(agentTag);
|
||||
return match ? { type: 'artemisTag', raw: match[0] } : undefined;
|
||||
}
|
||||
// Use one scoped parser for notes, plans, reports and streaming text. HTML from
|
||||
// models stays literal; Angular also sanitizes every innerHTML binding.
|
||||
const markdown = new Marked({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
async: false,
|
||||
extensions: [
|
||||
{ name: 'artemisTag', level: 'block', tokenizer: agentTagToken, renderer: () => '' },
|
||||
{
|
||||
name: 'artemisTag', level: 'inline', tokenizer: agentTagToken,
|
||||
start: src => src.search(/<\/?(?:thought|short_term_memory|strong_term_memory|reasoning|plan|task_plan|call_tool)\b/i)
|
||||
}
|
||||
],
|
||||
renderer: {
|
||||
html({ text }) { return escapeHtml(text); },
|
||||
code({ text, lang }) {
|
||||
const language = (lang || '').split(/\s/)[0].replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
return `<pre class="code-block"><code class="language-${language}">${escapeHtml(text)}</code></pre>\n`;
|
||||
},
|
||||
codespan({ text }) { return `<code class="inline-code">${escapeHtml(text)}</code>`; },
|
||||
link(token) {
|
||||
if (!safeMarkdownUrl(token.href)) return this.parser.parseInline(token.tokens);
|
||||
return `<a href="${escapeHtml(token.href)}"${token.title ? ` title="${escapeHtml(token.title)}"` : ''} target="_blank" rel="noopener noreferrer">${this.parser.parseInline(token.tokens)}</a>`;
|
||||
},
|
||||
image(token) {
|
||||
if (!safeMarkdownUrl(token.href, true)) return escapeHtml(token.text);
|
||||
return `<img src="${escapeHtml(token.href)}" alt="${escapeHtml(token.text)}"${token.title ? ` title="${escapeHtml(token.title)}"` : ''} loading="lazy" />`;
|
||||
},
|
||||
table(token) {
|
||||
return `<div class="md-table-scroll">${baseRenderer.table.call(this, token)}</div>`;
|
||||
},
|
||||
tablecell(token) {
|
||||
const tag = token.header ? 'th' : 'td';
|
||||
const align = token.align ? ` class="md-align-${token.align}"` : '';
|
||||
return `<${tag}${align}>${this.parser.parseInline(token.tokens)}</${tag}>`;
|
||||
},
|
||||
checkbox({ checked }) {
|
||||
return `<span class="md-task-marker ${checked ? 'checked' : 'unchecked'}" role="img" aria-label="${checked ? '已完成' : '未完成'}">${checked ? '☑' : '☐'}</span> `;
|
||||
},
|
||||
listitem(token) {
|
||||
let html = baseRenderer.listitem.call(this, token);
|
||||
if (token.task) return html.replace('<li>', `<li class="md-task-item ${token.checked ? 'checked' : 'unchecked'}">`);
|
||||
if (/^\[\/\]\s/.test(token.text)) {
|
||||
return html.replace(/^<li>(<p>)?\[\/\]\s*/, '<li class="md-task-item progress">$1<span class="md-task-marker progress" role="img" aria-label="进行中">◧</span> ');
|
||||
}
|
||||
const check = token.text.match(/^(verify|assert)(@end)?\s*:\s*/i);
|
||||
if (check) {
|
||||
const kind = check[1].toLowerCase();
|
||||
html = html.replace(/^<li>/, '<li class="md-verify-item">');
|
||||
return html.replace(/^(<li[^>]*>(?:<p>)?)(verify|assert)(@end)?\s*:\s*/i,
|
||||
`$1<span class="verify-badge${kind === 'assert' ? ' assert' : ''}">${kind}${check[2] || ''}</span>`);
|
||||
}
|
||||
if (/^finding\s*:/i.test(token.text)) {
|
||||
return html.replace(/^<li>(<p>)?finding\s*:\s*/i,
|
||||
'<li class="md-finding-item">$1<span class="finding-badge">finding</span>');
|
||||
}
|
||||
return html;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/** Render CommonMark/GFM while preserving ARTEMIS plan status markers. */
|
||||
export function renderMarkdownToHtml(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
// Clean up internal agent XML-like tags (e.g. <thought>, <short_term_memory>, etc.)
|
||||
const cleanedText = text
|
||||
.replace(/<\/?(thought|short_term_memory|strong_term_memory|reasoning|plan|task_plan|call_tool)[^\n>]*>?/gi, '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
|
||||
// Escape HTML first to prevent XSS
|
||||
const escaped = cleanedText
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
const rawLines = escaped.split('\n');
|
||||
const result: string[] = [];
|
||||
let currentListType: 'ul' | 'ol' | null = null;
|
||||
let inCodeBlock = false;
|
||||
let codeBlockLang = '';
|
||||
let codeBlockLines: string[] = [];
|
||||
|
||||
const closeList = () => {
|
||||
if (currentListType) {
|
||||
result.push(`</${currentListType}>`);
|
||||
currentListType = null;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < rawLines.length; i++) {
|
||||
const rawLine = rawLines[i];
|
||||
const trimmed = rawLine.trim();
|
||||
|
||||
// 1. Code blocks: ```lang ... ```
|
||||
if (trimmed.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// End code block
|
||||
const codeContent = codeBlockLines.join('\n');
|
||||
result.push(`<pre class="code-block"><code class="lang-${codeBlockLang.replace(/[^a-zA-Z0-9_-]/g, '')}">${codeContent}</code></pre>`);
|
||||
inCodeBlock = false;
|
||||
codeBlockLines = [];
|
||||
codeBlockLang = '';
|
||||
} else {
|
||||
closeList();
|
||||
inCodeBlock = true;
|
||||
codeBlockLang = trimmed.substring(3).trim();
|
||||
codeBlockLines = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockLines.push(rawLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Markdown result tables need real cells to remain readable in reports.
|
||||
if (trimmed.includes('|') && i + 1 < rawLines.length
|
||||
&& /^\s*\|?\s*:?-{3,}:?(?:\s*\|\s*:?-{3,}:?)*\s*\|?\s*$/.test(rawLines[i + 1])) {
|
||||
closeList();
|
||||
const cells = (line: string) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
||||
const headings = cells(rawLine);
|
||||
result.push('<div class="md-table-scroll"><table><thead><tr>'
|
||||
+ headings.map(cell => `<th>${formatInlineMarkdown(cell.trim())}</th>`).join('')
|
||||
+ '</tr></thead><tbody>');
|
||||
i += 2;
|
||||
while (i < rawLines.length && rawLines[i].trim() && rawLines[i].includes('|')) {
|
||||
const row = cells(rawLines[i]);
|
||||
result.push('<tr>' + headings.map((_, index) => `<td>${formatInlineMarkdown((row[index] || '').trim())}</td>`).join('') + '</tr>');
|
||||
i++;
|
||||
}
|
||||
result.push('</tbody></table></div>');
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Headings: # to ######
|
||||
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
||||
if (headingMatch) {
|
||||
closeList();
|
||||
const level = headingMatch[1].length;
|
||||
const headingContent = formatInlineMarkdown(headingMatch[2]);
|
||||
result.push(`<h${level}>${headingContent}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Horizontal Rule: ---, ***, ___
|
||||
if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
||||
closeList();
|
||||
result.push('<hr class="md-hr" />');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Blockquotes: > quote
|
||||
if (trimmed.startsWith('> ') || trimmed.startsWith('>')) {
|
||||
closeList();
|
||||
const quoteContent = formatInlineMarkdown(trimmed.replace(/^>\s?/, ''));
|
||||
result.push(`<blockquote>${quoteContent}</blockquote>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Unordered List: - item or * item
|
||||
const ulMatch = rawLine.match(/^(\s*)([\*\-])\s+(.*)$/);
|
||||
if (ulMatch) {
|
||||
if (currentListType !== 'ul') {
|
||||
closeList();
|
||||
currentListType = 'ul';
|
||||
result.push('<ul>');
|
||||
}
|
||||
const spaces = ulMatch[1].length;
|
||||
const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
|
||||
const indent = subLevel * 12;
|
||||
const style = indent ? ` style="margin-left: ${indent}px"` : '';
|
||||
const itemText = ulMatch[3];
|
||||
const checkMatch = itemText.match(/^(verify|assert)(@end)?\s*:\s*(.*)$/i);
|
||||
const findingMatch = itemText.match(/^finding\s*:\s*(.*)$/i);
|
||||
if (checkMatch) {
|
||||
const kind = checkMatch[1].toLowerCase();
|
||||
const atEnd = checkMatch[2] ? '@end' : '';
|
||||
const assertClass = kind === 'assert' ? ' assert' : '';
|
||||
const itemContent = formatInlineMarkdown(checkMatch[3].trim());
|
||||
result.push(`<li${style} class="md-verify-item"><span class="verify-badge${assertClass}">${kind}${atEnd}</span>${itemContent}</li>`);
|
||||
} else if (findingMatch) {
|
||||
const itemContent = formatInlineMarkdown(findingMatch[1].trim());
|
||||
result.push(`<li${style} class="md-finding-item"><span class="finding-badge">finding</span>${itemContent}</li>`);
|
||||
} else {
|
||||
const itemContent = formatInlineMarkdown(itemText);
|
||||
result.push(`<li${style}>${itemContent}</li>`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 6. Ordered List: 1. item
|
||||
const olMatch = rawLine.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
||||
if (olMatch) {
|
||||
if (currentListType !== 'ol') {
|
||||
closeList();
|
||||
currentListType = 'ol';
|
||||
result.push('<ol>');
|
||||
}
|
||||
const spaces = olMatch[1].length;
|
||||
const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
|
||||
const indent = subLevel * 12;
|
||||
const style = indent ? ` style="margin-left: ${indent}px"` : '';
|
||||
const itemContent = formatInlineMarkdown(olMatch[3]);
|
||||
result.push(`<li${style}>${itemContent}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 7. Regular paragraph / empty line
|
||||
closeList();
|
||||
if (trimmed === '') {
|
||||
result.push('<div class="md-para-gap"></div>');
|
||||
} else {
|
||||
const lineContent = formatInlineMarkdown(rawLine);
|
||||
result.push(`<div>${lineContent}</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
const codeContent = codeBlockLines.join('\n');
|
||||
result.push(`<pre class="code-block"><code class="lang-${codeBlockLang.replace(/[^a-zA-Z0-9_-]/g, '')}">${codeContent}</code></pre>`);
|
||||
}
|
||||
|
||||
closeList();
|
||||
return result.join('\n');
|
||||
return markdown.parse(text.replace(/\r\n?/g, '\n'), { async: false });
|
||||
}
|
||||
@@ -68,4 +68,37 @@ describe('plugin evidence timeline cards', () => {
|
||||
expect(getPluginToolView(tool)?.status).toBe('已执行');
|
||||
expect(getPluginToolView(tool)?.result).toBe('已点击');
|
||||
});
|
||||
|
||||
it('presents CUA window facts without modifying either raw result channel', () => {
|
||||
const tree = '- Window "Demo"\n - [0] Button "Save"';
|
||||
const raw = { execution: 'completed', content: [{ type: 'text', text: tree }],
|
||||
structured_content: { window_title: 'Demo', pid: 12, window_id: 34,
|
||||
returned_element_count: 0, total_element_count: 20, screenshot_width: 1280,
|
||||
screenshot_height: 720, snapshot_id: 's00000001', truncated: true, elements_complete: false,
|
||||
tree_markdown: tree, capture_focus: { target_foreground_after: false } } };
|
||||
const tool = { name: 'plug_abcdef1234_get_window_state', payload: {
|
||||
plugin_id: 'cua-driver', effect: 'read', observation: true, result: JSON.stringify(raw) } };
|
||||
const original = JSON.stringify(tool);
|
||||
const view = getPluginToolView(tool)!;
|
||||
expect(view.title).toBe('观察电脑窗口');
|
||||
expect(view.facts).toContain({ key: '窗口', value: 'Demo' });
|
||||
expect(view.facts).toContain({ key: '控件', value: '0 / 20' });
|
||||
expect(view.facts).toContain({ key: '截图', value: '1280 × 720' });
|
||||
expect(view.facts).toContain({ key: '控件树完整性', value: '未声明完整' });
|
||||
expect(view.facts).toContain({ key: '采集后焦点', value: '目标窗口不在前台' });
|
||||
expect(JSON.parse(view.rawResult)).toEqual(raw);
|
||||
expect(view.result).toBe(tree);
|
||||
expect(JSON.stringify(tool)).toBe(original);
|
||||
});
|
||||
|
||||
it('shows the actual post-action observation and preserves plain legacy receipts', () => {
|
||||
const view = getPluginToolView({ name: 'plug_abcdef1234_press_key', payload: {
|
||||
plugin_id: 'cua-driver', result: { structured_content: {
|
||||
observation: { window_title: 'After key', window_id: 56 } } } } })!;
|
||||
expect(view.facts).toContain({ key: '窗口', value: 'After key' });
|
||||
const legacy = getPluginToolView({ action: 'plug_abcdef1234_get_window_state',
|
||||
result: '来源:cua-driver\n窗口已采集' })!;
|
||||
expect(legacy.result).toBe('来源:cua-driver\n窗口已采集');
|
||||
expect(legacy.rawResult).toBe(legacy.result);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@ export interface PluginToolView {
|
||||
result: string;
|
||||
status: string;
|
||||
args: Array<{ key: string; value: string }>;
|
||||
facts: Array<{ key: string; value: string }>;
|
||||
rawResult: string;
|
||||
}
|
||||
|
||||
function objectValue(value: any): Record<string, any> {
|
||||
@@ -18,7 +20,8 @@ function objectValue(value: any): Record<string, any> {
|
||||
export function getPluginToolView(tool: any): PluginToolView | null {
|
||||
if (!tool || typeof tool !== 'object' || tool.type === 'llm_call') return null;
|
||||
const payload = objectValue(tool.payload);
|
||||
const result = objectValue(payload['result'] ?? tool.result);
|
||||
const rawResult = payload['result'] ?? tool.result;
|
||||
const result = objectValue(rawResult);
|
||||
const pluginId = payload['plugin_id'] || result['plugin_id'] || tool.plugin_id;
|
||||
if (!pluginId && !/^plug_[a-f0-9]{10}_/.test(tool.name || tool.action || '')) {
|
||||
return null;
|
||||
@@ -29,7 +32,8 @@ export function getPluginToolView(tool: any): PluginToolView | null {
|
||||
['possible', 'confirmed'].includes(result['side_effect']) ? 'write' : '');
|
||||
const auxiliary = effect !== 'write';
|
||||
const titles: Record<string, string> = {
|
||||
get_app_state: '观察电脑窗口', list_apps: '列出电脑应用',
|
||||
get_app_state: '观察电脑窗口', get_window_state: '观察电脑窗口',
|
||||
list_apps: '列出电脑应用', list_windows: '列出电脑窗口',
|
||||
click: '点击', double_click: '双击', drag: '拖动', scroll: '滚动',
|
||||
type_text: '输入文本', press_key: '按键', screenshot: '截取屏幕',
|
||||
save_screenshot: '保存截图', launch_app: '打开应用'
|
||||
@@ -37,6 +41,30 @@ export function getPluginToolView(tool: any): PluginToolView | null {
|
||||
const content = Array.isArray(result['content']) ? result['content'] : [];
|
||||
const text = content.filter((item: any) => item?.type === 'text' && typeof item.text === 'string')
|
||||
.map((item: any) => item.text).join('\n');
|
||||
const structured = objectValue(result['structured_content'] ?? result['structuredContent']);
|
||||
const observation = Object.keys(objectValue(structured['observation'])).length
|
||||
? objectValue(structured['observation']) : structured;
|
||||
const facts: PluginToolView['facts'] = [];
|
||||
const addFact = (key: string, value: any) => {
|
||||
if (value !== undefined && value !== null && value !== '') facts.push({ key, value: String(value) });
|
||||
};
|
||||
addFact('窗口', observation['window_title']);
|
||||
addFact('进程 PID', observation['pid']);
|
||||
addFact('窗口 ID', observation['window_id']);
|
||||
const count = observation['returned_element_count'] ?? observation['element_count'];
|
||||
addFact('控件', count === undefined ? undefined : observation['total_element_count'] === undefined
|
||||
? count : `${count} / ${observation['total_element_count']}`);
|
||||
if (observation['screenshot_width'] && observation['screenshot_height']) {
|
||||
addFact('截图', `${observation['screenshot_width']} × ${observation['screenshot_height']}`);
|
||||
}
|
||||
addFact('快照', observation['snapshot_id']);
|
||||
if (observation['truncated'] === true) addFact('控件范围', '返回已截断');
|
||||
if (observation['elements_complete'] === false) addFact('控件树完整性', '未声明完整');
|
||||
const focus = objectValue(observation['capture_focus']);
|
||||
if (typeof focus['target_foreground_after'] === 'boolean') {
|
||||
addFact('采集后焦点', focus['target_foreground_after'] ? '目标窗口在前台' : '目标窗口不在前台');
|
||||
}
|
||||
const plainResult = typeof rawResult === 'string' && Object.keys(result).length === 0 ? rawResult : '';
|
||||
const state = result['execution'];
|
||||
const status = tool.status === 'running' ? '执行中' :
|
||||
result['status'] === 'uncertain' || state === 'unknown' ? '结果不确定' :
|
||||
@@ -48,11 +76,14 @@ export function getPluginToolView(tool: any): PluginToolView | null {
|
||||
kind: payload['observation'] === true ? '辅助观察' : effect === 'read' ? '辅助读取' :
|
||||
effect === 'write' ? '插件操作' : '插件调用',
|
||||
auxiliary,
|
||||
result: [result['message'], text].filter(Boolean).join('\n') || String(tool.error || ''),
|
||||
result: [result['message'], text].filter(Boolean).join('\n') || plainResult || String(tool.error || ''),
|
||||
status,
|
||||
args: Object.entries(objectValue(payload['args'] ?? tool.args)).map(([key, value]) => ({
|
||||
key, value: typeof value === 'string' ? value : JSON.stringify(value)
|
||||
}))
|
||||
})),
|
||||
facts,
|
||||
rawResult: rawResult === undefined || rawResult === null ? '' :
|
||||
Object.keys(result).length ? JSON.stringify(result, null, 2) : String(rawResult)
|
||||
};
|
||||
return view;
|
||||
}
|
||||
@@ -21,6 +21,17 @@
|
||||
<title>artemis</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script>
|
||||
// Apply before the first paint, including when local storage is unavailable.
|
||||
(() => {
|
||||
let preference = 'system';
|
||||
try { preference = localStorage.getItem('artemis.theme') || 'system'; } catch {}
|
||||
const dark = preference === 'dark' || (preference !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
||||
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
||||
})();
|
||||
</script>
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/logo.png?v=2">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=2">
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg?v=2">
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@use './theme';
|
||||
@use './markdown';
|
||||
@import '@angular/cdk/overlay-prebuilt.css';
|
||||
|
||||
// Keep fonts local so production builds and icons do not depend on Google Fonts.
|
||||
@@ -85,8 +87,8 @@ body, input, button, select, textarea {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #1e293b;
|
||||
background-color: #f8f9fa;
|
||||
color: var(--app-text-control, #1e293b);
|
||||
background-color: var(--app-canvas, #f8f9fa);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
Reference in new issue
Block a user