diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html index f4b4bd1..0bb4fc2 100644 --- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html @@ -277,6 +277,30 @@ limitations under the License. } } + + @if (milestone.checks.length > 0) { +
+ @for (check of milestone.checks; track trackMarkdownLine($index, check)) { +
+
+ {{ check.checkKind || 'verify' }}{{ check.atEnd ? ' @end' : '' }} +
+
+ @for (seg of check.segments; track seg) { + {{ seg.text }} + } +
+
+ } +
+ } } @@ -285,36 +309,52 @@ limitations under the License. @if (parsedNote().otherLines.length > 0) {
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) { -
- @switch (line.type) { - @case ('checked') { - check_box - } - @case ('progress') { - indeterminate_check_box - } - @case ('unchecked') { - check_box_outline_blank - } - @case ('list-item') { - • - } - } - @if (line.type !== 'empty') { - + @if (line.type === 'verify' || line.type === 'finding') { +
+
+ {{ line.checkKind || 'verify' }}{{ line.atEnd ? ' @end' : '' }} +
+
@for (seg of line.segments; track seg) { {{ seg.text }} } - - } -
+
+
+ } @else { +
+ @switch (line.type) { + @case ('checked') { + check_box + } + @case ('progress') { + indeterminate_check_box + } + @case ('unchecked') { + check_box_outline_blank + } + @case ('list-item') { + • + } + } + @if (line.type !== 'empty') { + + @for (seg of line.segments; track seg) { + {{ seg.text }} + } + + } +
+ } }
} @@ -1110,6 +1150,12 @@ limitations under the License.
@for (line of parsedOut.otherLines; track trackMarkdownLine($index, line)) {

+ @if (line.type === 'verify') { + {{ line.checkKind || 'verify' }}{{ line.atEnd ? '@end' : '' }} + } + @if (line.type === 'finding') { + finding + } @for (seg of line.segments; track seg) { @if (seg.bold) { {{ seg.text }} diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss index 169ec72..b12e560 100644 --- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss @@ -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; + } } } diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html index baf3a39..5d9d053 100644 --- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +++ b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html @@ -290,6 +290,30 @@ limitations under the License. }

} + + @if (milestone.checks.length > 0) { +
+ @for (check of milestone.checks; track trackMarkdownLine($index, check)) { +
+
+ {{ check.checkKind || 'verify' }}{{ check.atEnd ? ' @end' : '' }} +
+
+ @for (seg of check.segments; track seg) { + {{ seg.text }} + } +
+
+ } +
+ } } @@ -298,36 +322,52 @@ limitations under the License. @if (parsedNote().otherLines.length > 0) {
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) { -
- @switch (line.type) { - @case ('checked') { - check_box - } - @case ('progress') { - indeterminate_check_box - } - @case ('unchecked') { - check_box_outline_blank - } - @case ('list-item') { - • - } - } - @if (line.type !== 'empty') { - + @if (line.type === 'verify' || line.type === 'finding') { +
+
+ {{ line.checkKind || 'verify' }}{{ line.atEnd ? ' @end' : '' }} +
+
@for (seg of line.segments; track seg) { {{ seg.text }} } - - } -
+
+
+ } @else { +
+ @switch (line.type) { + @case ('checked') { + check_box + } + @case ('progress') { + indeterminate_check_box + } + @case ('unchecked') { + check_box_outline_blank + } + @case ('list-item') { + • + } + } + @if (line.type !== 'empty') { + + @for (seg of line.segments; track seg) { + {{ seg.text }} + } + + } +
+ } }
} diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss index 577ac88..2de9b95 100644 --- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +++ b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss @@ -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 { diff --git a/apps/showcase_ui/src/app/core/models/markdown.model.ts b/apps/showcase_ui/src/app/core/models/markdown.model.ts index b748481..10b9add 100644 --- a/apps/showcase_ui/src/app/core/models/markdown.model.ts +++ b/apps/showcase_ui/src/app/core/models/markdown.model.ts @@ -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 { diff --git a/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts b/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts new file mode 100644 index 0000000..0fd04de --- /dev/null +++ b/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts @@ -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('verify'); + expect(html).toContain('status is ok'); + }); +}); diff --git a/apps/showcase_ui/src/app/utils/markdown-parser.util.ts b/apps/showcase_ui/src/app/utils/markdown-parser.util.ts index bc8ca44..0b3e377 100644 --- a/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +++ b/apps/showcase_ui/src/app/utils/markdown-parser.util.ts @@ -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)) { - currentMilestone.subSteps.push(line); + 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]); - result.push(`${itemContent}`); + 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(`${kind}${atEnd}${itemContent}`); + } else if (findingMatch) { + const itemContent = formatInlineMarkdown(findingMatch[1].trim()); + result.push(`finding${itemContent}`); + } else { + const itemContent = formatInlineMarkdown(itemText); + result.push(`${itemContent}`); + } continue; }