Skip to content

Commit 377f9fd

Browse files
committed
Markdown parsing
1 parent 4da64ee commit 377f9fd

6 files changed

Lines changed: 174 additions & 22 deletions

File tree

bun.lock

Lines changed: 21 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@ng-icons/heroicons": "^33.0.0",
2828
"@openworkers/croner-wasm": "^0.3.1",
2929
"dexie": "^4.2.1",
30+
"marked": "^17.0.1",
3031
"monaco-editor": "0.55.1",
3132
"rxjs": "^7.8.2",
3233
"tslib": "^2.8.1"

src/app/modules/worker/pages/worker-edit/components/ai-chat/ai-chat.component.css

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
}
186186

187187
.cli-content {
188-
white-space: pre-wrap;
188+
white-space: unset;
189189
word-break: break-word;
190190
}
191191

@@ -642,3 +642,122 @@
642642
border-color: var(--border);
643643
color: var(--text-hint);
644644
}
645+
646+
/* Markdown content styling - ::ng-deep needed for innerHTML content */
647+
.cli-markdown {
648+
display: block;
649+
}
650+
651+
::ng-deep .cli-markdown > p {
652+
margin: 0 0 8px 0;
653+
}
654+
655+
::ng-deep .cli-markdown > p:last-child {
656+
margin-bottom: 0;
657+
}
658+
659+
::ng-deep .cli-markdown code {
660+
background: var(--bg-secondary);
661+
padding: 2px 6px;
662+
border-radius: 3px;
663+
font-size: 12px;
664+
}
665+
666+
::ng-deep .cli-markdown > pre {
667+
background: var(--bg-secondary);
668+
padding: 12px;
669+
border-radius: 6px;
670+
overflow-x: auto;
671+
margin: 8px 0;
672+
}
673+
674+
::ng-deep .cli-markdown > pre code {
675+
background: none;
676+
padding: 0;
677+
}
678+
679+
::ng-deep .cli-markdown > ul,
680+
::ng-deep .cli-markdown > ol {
681+
margin: 8px 0;
682+
padding-left: 8px;
683+
list-style: none;
684+
}
685+
686+
::ng-deep .cli-markdown li {
687+
margin: 4px 0;
688+
position: relative;
689+
padding-left: 16px;
690+
}
691+
692+
::ng-deep .cli-markdown > ul > li::before {
693+
content: '•';
694+
position: absolute;
695+
left: 0;
696+
color: var(--text-muted);
697+
}
698+
699+
::ng-deep .cli-markdown > ol {
700+
counter-reset: list-counter;
701+
}
702+
703+
::ng-deep .cli-markdown > ol > li {
704+
counter-increment: list-counter;
705+
}
706+
707+
::ng-deep .cli-markdown > ol > li::before {
708+
content: counter(list-counter) '.';
709+
position: absolute;
710+
left: 0;
711+
color: var(--text-muted);
712+
font-size: 0.9em;
713+
}
714+
715+
::ng-deep .cli-markdown strong {
716+
font-weight: 600;
717+
}
718+
719+
::ng-deep .cli-markdown a {
720+
color: var(--accent);
721+
text-decoration: none;
722+
}
723+
724+
::ng-deep .cli-markdown a:hover {
725+
text-decoration: underline;
726+
}
727+
728+
::ng-deep .cli-markdown > blockquote {
729+
border-left: 3px solid var(--border);
730+
margin: 8px 0;
731+
padding-left: 12px;
732+
color: var(--text-muted);
733+
}
734+
735+
::ng-deep .cli-markdown > h1,
736+
::ng-deep .cli-markdown > h2,
737+
::ng-deep .cli-markdown > h3,
738+
::ng-deep .cli-markdown > h4 {
739+
margin: 8px 0 4px 0;
740+
font-weight: 600;
741+
}
742+
743+
::ng-deep .cli-markdown > h1 {
744+
font-size: 1.15em;
745+
}
746+
747+
::ng-deep .cli-markdown > h2 {
748+
font-size: 1.1em;
749+
}
750+
751+
::ng-deep .cli-markdown > h3 {
752+
font-size: 1.05em;
753+
}
754+
755+
::ng-deep .cli-markdown > h4 {
756+
font-size: 1em;
757+
}
758+
759+
::ng-deep .cli-markdown > hr {
760+
border: none;
761+
border-top: 1px solid var(--border);
762+
margin: 12px 0;
763+
}

src/app/modules/worker/pages/worker-edit/components/ai-chat/ai-chat.component.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,19 @@
5959
@for (message of store.messages(); track $index) {
6060
<div class="cli-message" [class.cli-user]="message.role === 'user'">
6161
<span class="cli-prompt">{{ message.role === 'user' ? '>' : '◆' }}</span>
62-
<span class="cli-content">{{ message.content }}</span>
62+
@if (message.role === 'user') {
63+
<span class="cli-content">{{ message.content }}</span>
64+
} @else {
65+
<span class="cli-content cli-markdown" [innerHTML]="message.content | markdown"></span>
66+
}
6367
</div>
6468
}
6569

6670
<!-- Streaming text with indicator -->
6771
@if (store.streamingText()) {
6872
<div class="cli-message">
6973
<span class="cli-prompt"></span>
70-
<span class="cli-content">{{ store.streamingText() }}</span>
74+
<span class="cli-content cli-markdown" [innerHTML]="store.streamingText() | markdown"></span>
7175
@if (store.isStreaming()) {
7276
<span class="streaming-indicator"> <span></span><span></span><span></span> </span>
7377
}

src/app/modules/worker/pages/worker-edit/components/ai-chat/ai-chat.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import { EditorStateService } from '~/app/services/editor-state.service';
1616
import { firstValueFrom, Subscription } from 'rxjs';
1717
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
1818
import { NgIconComponent } from '@ng-icons/core';
19+
import { MarkdownPipe } from '~/app/pipes/markdown.pipe';
1920

2021
const CLAUDE_TOKEN_KEY = 'claude_token';
2122

2223
@Component({
2324
standalone: true,
24-
imports: [CommonModule, ReactiveFormsModule, FormsModule, RouterLink, MonacoEditorModule, NgIconComponent],
25+
imports: [CommonModule, ReactiveFormsModule, FormsModule, RouterLink, MonacoEditorModule, NgIconComponent, MarkdownPipe],
2526
selector: 'app-ai-chat',
2627
templateUrl: './ai-chat.component.html',
2728
styleUrls: ['./ai-chat.component.css']

src/app/pipes/markdown.pipe.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
import { marked } from 'marked';
3+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
4+
5+
@Pipe({
6+
name: 'markdown',
7+
standalone: true
8+
})
9+
export class MarkdownPipe implements PipeTransform {
10+
constructor(private sanitizer: DomSanitizer) {
11+
// Configure marked for inline rendering (no <p> wrapper for single lines)
12+
marked.setOptions({
13+
breaks: true,
14+
gfm: true
15+
});
16+
}
17+
18+
transform(value: string | null | undefined): SafeHtml {
19+
if (!value) return '';
20+
21+
const html = marked.parse(value, { async: false }) as string;
22+
return this.sanitizer.bypassSecurityTrustHtml(html);
23+
}
24+
}

0 commit comments

Comments
 (0)