- Maximale hoogte voor AI message in ChatInput nu geldig voor zowel desktop als mobile devices.
- Correctie marked component in SideBarExplanation.vue - AI messages ondersteunen nu markdown. Markdown rendering is als een centrale utility gedefinieerd.
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Bericht tekst -->
|
||||
<div v-if="message.content" class="message-text" v-html="formatMessage(message.content)"></div>
|
||||
<div v-if="message.content" class="message-text" v-html="renderedMessage"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -126,6 +126,7 @@ import DynamicForm from './DynamicForm.vue';
|
||||
import ProgressTracker from './ProgressTracker.vue';
|
||||
import { useIconManager } from '../js/composables/useIconManager.js';
|
||||
import { useComponentTranslations } from '../js/services/LanguageProvider.js';
|
||||
import { renderMarkdown } from '../js/utils/markdownRenderer.js';
|
||||
|
||||
export default {
|
||||
name: 'ChatMessage',
|
||||
@@ -236,6 +237,31 @@ export default {
|
||||
// Component cleanup if needed
|
||||
},
|
||||
computed: {
|
||||
renderedMessage() {
|
||||
const content = this.message?.content ?? '';
|
||||
if (!content) return '';
|
||||
// Only render markdown for AI messages and text type
|
||||
if (this.message.sender !== 'ai' || this.message.type !== 'text') {
|
||||
// plain text fallback
|
||||
return String(content)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
return renderMarkdown(content, {
|
||||
allowInlineHTML: false,
|
||||
enableTables: true,
|
||||
enableBreaks: true,
|
||||
enableImages: false,
|
||||
enableCodeBlocks: false,
|
||||
allowInlineCode: false,
|
||||
linkTargetBlank: true,
|
||||
sidebarAccent: false
|
||||
});
|
||||
},
|
||||
isActiveContext() {
|
||||
// active if in input area or sticky area
|
||||
return !!(this.isInInputArea || this.isInStickyArea);
|
||||
@@ -600,12 +626,64 @@ export default {
|
||||
|
||||
/* Zorgt dat het lettertype consistent is */
|
||||
.message-text {
|
||||
color: var(--ai-message-text-color);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Markdown typography inside message text */
|
||||
.message-text :deep(h1),
|
||||
.message-text :deep(h2),
|
||||
.message-text :deep(h3),
|
||||
.message-text :deep(h4) {
|
||||
margin: 0.8rem 0 0.4rem;
|
||||
color: var(--ai-message-text-color);
|
||||
}
|
||||
.message-text :deep(p) {
|
||||
margin: 0 0 0.6rem 0;
|
||||
}
|
||||
.message-text :deep(ul),
|
||||
.message-text :deep(ol) {
|
||||
padding-left: 1.2rem;
|
||||
margin: 0.4rem 0 0.6rem;
|
||||
}
|
||||
.message-text :deep(li) {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
.message-text :deep(a) {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.message-text :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.6rem 0;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
.message-text :deep(th),
|
||||
.message-text :deep(td) {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.message-text :deep(th) {
|
||||
background: rgba(0,0,0,0.05);
|
||||
font-weight: 600;
|
||||
}
|
||||
.message-text :deep(blockquote) {
|
||||
border-left: 3px solid var(--primary-color);
|
||||
padding-left: 10px;
|
||||
margin: 0.6rem 0;
|
||||
color: var(--ai-message-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
.message-text :deep(hr) {
|
||||
border: 0; border-top: 1px solid #ddd; margin: 0.8rem 0;
|
||||
}
|
||||
|
||||
/* Form error styling */
|
||||
.form-error {
|
||||
color: red;
|
||||
@@ -706,26 +784,21 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile bubble height constraints and inner scroll containment */
|
||||
@media (max-width: 768px) {
|
||||
/* Default/history: apply to all message bubbles */
|
||||
.message .message-content {
|
||||
max-height: 33vh; /* fallback */
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain; /* prevent scroll chaining to parent */
|
||||
-webkit-overflow-scrolling: touch; /* iOS smooth inertia */
|
||||
}
|
||||
/* Active contexts (input area or sticky area): allow up to half viewport */
|
||||
.message.input-area .message-content,
|
||||
.message.sticky-area .message-content {
|
||||
max-height: 50vh; /* fallback */
|
||||
}
|
||||
/* Bubble height constraints and inner scroll containment (apply on all viewports) */
|
||||
.message .message-content {
|
||||
max-height: 33vh; /* fallback */
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain; /* prevent scroll chaining to parent */
|
||||
-webkit-overflow-scrolling: touch; /* iOS smooth inertia */
|
||||
}
|
||||
/* Active contexts (input area or sticky area): allow up to half viewport */
|
||||
.message.input-area .message-content,
|
||||
.message.sticky-area .message-content {
|
||||
max-height: 50vh; /* fallback */
|
||||
}
|
||||
@supports (max-height: 1svh) {
|
||||
@media (max-width: 768px) {
|
||||
.message .message-content { max-height: 33svh; }
|
||||
.message.input-area .message-content,
|
||||
.message.sticky-area .message-content { max-height: 50svh; }
|
||||
}
|
||||
.message .message-content { max-height: 33svh; }
|
||||
.message.input-area .message-content,
|
||||
.message.sticky-area .message-content { max-height: 50svh; }
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useComponentTranslations } from '../js/services/LanguageProvider.js';
|
||||
import { renderMarkdown } from '../js/utils/markdownRenderer.js';
|
||||
|
||||
const props = defineProps({
|
||||
originalText: {
|
||||
@@ -44,19 +45,29 @@ const { texts: translations, isLoading, error, currentLanguage } = useComponentT
|
||||
originalTexts
|
||||
);
|
||||
|
||||
const translatedText = computed(() => translations.value?.explanation || props.originalText);
|
||||
const translatedText = computed(() => {
|
||||
const candidate = translations.value?.explanation ?? props.originalText ?? '';
|
||||
return typeof candidate === 'string' ? candidate : String(candidate ?? '');
|
||||
});
|
||||
|
||||
// Render markdown content
|
||||
// Render markdown content (defensive: always pass a string and catch errors)
|
||||
const renderedExplanation = computed(() => {
|
||||
if (!translatedText.value) return '';
|
||||
|
||||
// Use marked if available, otherwise return plain text
|
||||
if (typeof window.marked === 'function') {
|
||||
return window.marked(translatedText.value);
|
||||
} else if (window.marked && typeof window.marked.parse === 'function') {
|
||||
return window.marked.parse(translatedText.value.replace(/\[\[(.*?)\]\]/g, '<strong>$1</strong>'));
|
||||
} else {
|
||||
return translatedText.value;
|
||||
const text = translatedText.value || '';
|
||||
if (!text) return '';
|
||||
try {
|
||||
return renderMarkdown(text, {
|
||||
allowInlineHTML: false,
|
||||
enableTables: true,
|
||||
enableBreaks: true,
|
||||
enableImages: false,
|
||||
enableCodeBlocks: false,
|
||||
allowInlineCode: false,
|
||||
linkTargetBlank: true,
|
||||
sidebarAccent: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('Markdown render error in SideBarExplanation, falling back to plain text:', err);
|
||||
return String(text);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user