mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-20 23:08:14 +00:00
Enhancement: support markdown in AI chat (#14409)
This commit is contained in:
1 parent
20b7a116fd
commit
7b53a4b316
9 files changed
+207
-9
No files matched your search
@@ -5,14 +5,16 @@
|
||||
</button>
|
||||
<div ngbDropdownMenu class="dropdown-menu-end shadow p-3" aria-labelledby="chatDropdown">
|
||||
<div class="chat-container bg-light p-2">
|
||||
<div class="chat-messages font-monospace small">
|
||||
<div class="chat-messages small">
|
||||
@for (message of messages(); track message) {
|
||||
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
|
||||
<div class="p-2 m-2" [class.bg-body]="message.role === 'user'">
|
||||
<span class="text-break">
|
||||
{{ message.content }}
|
||||
@if (message.role === 'assistant') {
|
||||
<div class="chat-markdown text-break text-wrap" [innerHTML]="message.content | markdown"></div>
|
||||
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
|
||||
</span>
|
||||
} @else {
|
||||
<span class="text-break">{{ message.content }}</span>
|
||||
}
|
||||
@if (message.role === 'assistant' && message.references?.length) {
|
||||
<div class="chat-references list-group mt-3">
|
||||
@for (reference of message.references; track reference.id) {
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.chat-references {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
@@ -40,3 +36,19 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-markdown ::ng-deep {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -207,4 +207,33 @@ describe('ChatComponent', () => {
|
||||
component.searchInputKeyDown(event)
|
||||
expect(component.sendMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render markdown for assistant messages only', () => {
|
||||
component.messages.set([
|
||||
{ role: 'user', content: '**user**' },
|
||||
{ role: 'assistant', content: '**assistant** <script>x</script>' },
|
||||
])
|
||||
fixture.detectChanges()
|
||||
const el: HTMLElement = fixture.nativeElement
|
||||
expect(el.querySelector('.chat-markdown strong')?.textContent).toBe(
|
||||
'assistant'
|
||||
)
|
||||
expect(el.querySelector('.chat-markdown script')).toBeNull()
|
||||
expect(el.textContent).toContain('**user**')
|
||||
})
|
||||
|
||||
it('should keep the destination hostname visible after sanitizing chat links', () => {
|
||||
component.messages.set([
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
'[View original document](https://otherdomain.example/?data=secret)',
|
||||
},
|
||||
])
|
||||
fixture.detectChanges()
|
||||
const el: HTMLElement = fixture.nativeElement
|
||||
const link = el.querySelector('.chat-markdown a')
|
||||
expect(link?.textContent).toBe('View original document')
|
||||
expect(link?.nextElementSibling?.textContent).toBe('(otherdomain.example)')
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NavigationEnd, Router, RouterModule } from '@angular/router'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { MarkdownPipe } from 'src/app/pipes/markdown.pipe'
|
||||
import { filter, map } from 'rxjs'
|
||||
import {
|
||||
ChatMessage,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
RouterModule,
|
||||
NgxBootstrapIconsModule,
|
||||
NgbDropdownModule,
|
||||
MarkdownPipe,
|
||||
],
|
||||
templateUrl: './chat.component.html',
|
||||
styleUrl: './chat.component.scss',
|
||||
|
||||
Reference in new issue
Block a user