feat(showcase_ui): support verification and finding cards in agent notes
This commit is contained in:
1 parent
99343dfe55
commit
73d7b0c116
7 files changed
+444
-2
No files matched your search
@@ -277,6 +277,30 @@ limitations under the License.
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Verification Box (Pulled out into a minimalist framed card) -->
|
||||
@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>
|
||||
@@ -285,6 +309,21 @@ limitations under the License.
|
||||
@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"
|
||||
@@ -316,6 +355,7 @@ limitations under the License.
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1110,6 +1150,12 @@ limitations under the License.
|
||||
<div class="parsed-other-lines">
|
||||
@for (line of parsedOut.otherLines; track trackMarkdownLine($index, line)) {
|
||||
<p class="other-line" [class]="'line-' + line.type">
|
||||
@if (line.type === 'verify') {
|
||||
<span class="verify-badge" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? '@end' : '' }}</span>
|
||||
}
|
||||
@if (line.type === 'finding') {
|
||||
<span class="finding-badge">finding</span>
|
||||
}
|
||||
@for (seg of line.segments; track seg) {
|
||||
@if (seg.bold) {
|
||||
<strong>{{ seg.text }}</strong>
|
||||
|
||||
@@ -404,6 +404,76 @@ $stream-row-height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&.verify {
|
||||
color: #334155;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
|
||||
.verify-badge {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
|
||||
font-size: 10.5px;
|
||||
font-weight: 550;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: lowercase;
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
padding: 1.5px 6px;
|
||||
margin-top: 1.5px;
|
||||
user-select: none;
|
||||
|
||||
&.assert {
|
||||
color: #0369a1;
|
||||
background: #f0f9ff;
|
||||
border-color: #e0f2fe;
|
||||
}
|
||||
}
|
||||
|
||||
.line-text {
|
||||
color: #334155;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.finding {
|
||||
color: #92400e;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
|
||||
.finding-badge {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
|
||||
font-size: 10.5px;
|
||||
font-weight: 550;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: lowercase;
|
||||
color: #b45309;
|
||||
background: #fef3c7;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 4px;
|
||||
padding: 1.5px 6px;
|
||||
margin-top: 1.5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-text {
|
||||
color: #92400e;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
@@ -430,6 +500,84 @@ $stream-row-height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.milestone-checks-container {
|
||||
margin-top: 6px;
|
||||
padding-left: 26px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.verification-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 7px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
&.standalone {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.card-tag {
|
||||
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
|
||||
font-size: 9.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
|
||||
&.assert {
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #334155;
|
||||
word-break: break-word;
|
||||
|
||||
.bold-segment {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.code-segment {
|
||||
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
&.finding {
|
||||
background: #fffbeb;
|
||||
border-color: #fef3c7;
|
||||
|
||||
.card-header .card-tag {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
color: #92400e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.other-lines-container {
|
||||
@@ -447,6 +595,11 @@ $stream-row-height: 25px;
|
||||
font-size: 12.5px;
|
||||
color: #334155;
|
||||
line-height: 1.5;
|
||||
|
||||
.line-text {
|
||||
word-break: break-word;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1763,6 +1916,14 @@ $stream-row-height: 25px;
|
||||
color: #1f1f1f;
|
||||
margin: 4px 0;
|
||||
padding-left: 2px;
|
||||
|
||||
&.md-verify-item {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
&.md-finding-item {
|
||||
color: #92400e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,6 +290,30 @@ limitations under the License.
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Verification Box (Pulled out into a minimalist framed card) -->
|
||||
@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>
|
||||
@@ -298,6 +322,21 @@ limitations under the License.
|
||||
@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"
|
||||
@@ -329,6 +368,7 @@ limitations under the License.
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -878,6 +878,84 @@ $text-muted: #a1a1aa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.milestone-checks-container {
|
||||
margin-top: 8px;
|
||||
padding-left: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.verification-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&.standalone {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.card-tag {
|
||||
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
|
||||
&.assert {
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
font-family: inherit;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
word-break: break-word;
|
||||
|
||||
.bold-segment {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.code-segment {
|
||||
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
padding: 1.5px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
&.finding {
|
||||
background: #fffbeb;
|
||||
border-color: #fef3c7;
|
||||
|
||||
.card-header .card-tag {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
color: #92400e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.other-lines-container {
|
||||
|
||||
@@ -21,9 +21,11 @@ export interface MarkdownSegment {
|
||||
}
|
||||
|
||||
export interface MarkdownLine {
|
||||
type: 'h1' | 'h2' | 'h3' | 'checked' | 'progress' | 'unchecked' | 'list-item' | 'text' | 'empty';
|
||||
type: 'h1' | 'h2' | 'h3' | 'checked' | 'progress' | 'unchecked' | 'list-item' | 'verify' | 'assert' | 'finding' | 'text' | 'empty';
|
||||
segments: MarkdownSegment[];
|
||||
indent: number;
|
||||
checkKind?: 'verify' | 'assert' | 'finding';
|
||||
atEnd?: boolean;
|
||||
}
|
||||
|
||||
export interface NoteMilestone {
|
||||
@@ -31,6 +33,7 @@ export interface NoteMilestone {
|
||||
type: 'checked' | 'progress' | 'unchecked';
|
||||
segments: MarkdownSegment[];
|
||||
subSteps: MarkdownLine[];
|
||||
checks: MarkdownLine[];
|
||||
}
|
||||
|
||||
export interface ParsedNote {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { parseNoteLines, parseNote, renderMarkdownToHtml } from './markdown-parser.util';
|
||||
|
||||
describe('markdown-parser.util verification & check lines', () => {
|
||||
it('should parse - verify: line as type verify with checkKind verify', () => {
|
||||
const markdown = `- [x] Parent task
|
||||
- [x] Subtask 1
|
||||
- verify: Commute duration is recorded in note \`eta_details\``;
|
||||
|
||||
const lines = parseNoteLines(markdown);
|
||||
expect(lines.length).toBe(3);
|
||||
expect(lines[0].type).toBe('checked');
|
||||
expect(lines[1].type).toBe('checked');
|
||||
|
||||
const verifyLine = lines[2];
|
||||
expect(verifyLine.type).toBe('verify');
|
||||
expect(verifyLine.checkKind).toBe('verify');
|
||||
expect(verifyLine.atEnd).toBe(false);
|
||||
expect(verifyLine.segments.map(s => s.text).join('')).toBe('Commute duration is recorded in note eta_details');
|
||||
expect(verifyLine.segments.find(s => s.code)?.text).toBe('eta_details');
|
||||
});
|
||||
|
||||
it('should parse - assert: and - assert@end: lines', () => {
|
||||
const markdown = `- [ ] Open app
|
||||
- assert: the welcome screen shows up
|
||||
- assert@end: final status is completed`;
|
||||
|
||||
const lines = parseNoteLines(markdown);
|
||||
expect(lines.length).toBe(3);
|
||||
|
||||
expect(lines[1].type).toBe('verify');
|
||||
expect(lines[1].checkKind).toBe('assert');
|
||||
expect(lines[1].atEnd).toBe(false);
|
||||
|
||||
expect(lines[2].type).toBe('verify');
|
||||
expect(lines[2].checkKind).toBe('assert');
|
||||
expect(lines[2].atEnd).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse - finding: lines', () => {
|
||||
const markdown = `- [x] Step 1
|
||||
- finding: Unresolved verify failure`;
|
||||
|
||||
const lines = parseNoteLines(markdown);
|
||||
expect(lines[1].type).toBe('finding');
|
||||
expect(lines[1].checkKind).toBe('finding');
|
||||
expect(lines[1].segments[0].text).toBe('Unresolved verify failure');
|
||||
});
|
||||
|
||||
it('should group verify lines under parent milestone checks in parseNote', () => {
|
||||
const note = `- [x] Open Maps
|
||||
- [x] Search destination
|
||||
- verify: Arrival time is visible in note \`eta\``;
|
||||
|
||||
const parsed = parseNote(note);
|
||||
expect(parsed.milestones.length).toBe(1);
|
||||
expect(parsed.milestones[0].subSteps.length).toBe(1);
|
||||
expect(parsed.milestones[0].subSteps[0].type).toBe('checked');
|
||||
expect(parsed.milestones[0].checks.length).toBe(1);
|
||||
expect(parsed.milestones[0].checks[0].type).toBe('verify');
|
||||
expect(parsed.milestones[0].checks[0].checkKind).toBe('verify');
|
||||
});
|
||||
|
||||
it('should render verify badges in renderMarkdownToHtml', () => {
|
||||
const text = `- verify: status is ok`;
|
||||
const html = renderMarkdownToHtml(text);
|
||||
expect(html).toContain('class="md-verify-item"');
|
||||
expect(html).toContain('<span class="verify-badge">verify</span>');
|
||||
expect(html).toContain('status is ok');
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,31 @@ export function parseNoteLines(content: string): MarkdownLine[] {
|
||||
const rawText = trimmed.substring(6);
|
||||
return { type: 'unchecked', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
// Check lines (- verify: / - assert: / - verify@end: / - assert@end:)
|
||||
const checkMatch = trimmed.match(/^[-*]\s*(verify|assert)(@end)?\s*:\s*(.*)$/i);
|
||||
if (checkMatch) {
|
||||
const kind = checkMatch[1].toLowerCase() as 'verify' | 'assert';
|
||||
const atEnd = Boolean(checkMatch[2]);
|
||||
const rawText = checkMatch[3].trim();
|
||||
return {
|
||||
type: 'verify',
|
||||
checkKind: kind,
|
||||
atEnd,
|
||||
segments: parseMarkdownSegments(rawText),
|
||||
indent
|
||||
};
|
||||
}
|
||||
// System findings (- finding:)
|
||||
const findingMatch = trimmed.match(/^[-*]\s*finding\s*:\s*(.*)$/i);
|
||||
if (findingMatch) {
|
||||
const rawText = findingMatch[1].trim();
|
||||
return {
|
||||
type: 'finding',
|
||||
checkKind: 'finding',
|
||||
segments: parseMarkdownSegments(rawText),
|
||||
indent
|
||||
};
|
||||
}
|
||||
// Regular list item
|
||||
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
||||
const rawText = trimmed.substring(2);
|
||||
@@ -125,12 +150,17 @@ export function parseNote(content: string): ParsedNote {
|
||||
type: line.type as 'checked' | 'progress' | 'unchecked',
|
||||
segments: line.segments,
|
||||
subSteps: [],
|
||||
checks: [],
|
||||
_indent: line.indent
|
||||
};
|
||||
milestones.push(currentMilestone);
|
||||
} else if (currentMilestone) {
|
||||
if (line.indent > (currentMilestone._indent ?? 0)) {
|
||||
if (line.type === 'verify' || line.type === 'assert' || line.type === 'finding') {
|
||||
currentMilestone.checks.push(line);
|
||||
} else {
|
||||
currentMilestone.subSteps.push(line);
|
||||
}
|
||||
} else {
|
||||
otherLines.push(line);
|
||||
}
|
||||
@@ -297,8 +327,22 @@ export function renderMarkdownToHtml(text: string): string {
|
||||
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(ulMatch[3]);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user