diff --git a/DSL/Resql/users/POST/get-chat-by-id.sql b/DSL/Resql/users/POST/get-chat-by-id.sql index a7c3daaee..2784931bf 100644 --- a/DSL/Resql/users/POST/get-chat-by-id.sql +++ b/DSL/Resql/users/POST/get-chat-by-id.sql @@ -12,6 +12,15 @@ csa_title_config AS ( AND id IN (SELECT max(id) FROM configuration WHERE key = 'is_csa_title_visible' GROUP BY key) AND NOT deleted ), +chat_history_comments AS ( + SELECT + comment, + chat_id, + created, + author_display_name + FROM chat_history_comments + where id in (SELECT MAX(id) AS maxId FROM chat_history_comments GROUP BY chat_id) +), chatById AS( SELECT base_id, @@ -72,8 +81,12 @@ SELECT c.base_id AS id, ELSE '' END) AS csa_title, m.content AS last_message, - m.updated AS last_message_timestamp + m.updated AS last_message_timestamp, + s.comment, + s.created as comment_added_date, + s.author_display_name as comment_author FROM chatById AS c JOIN message AS m ON c.base_id = m.chat_base_id +LEFT JOIN chat_history_comments AS s ON s.chat_id = m.chat_base_id ORDER BY m.updated DESC LIMIT 1; diff --git a/GUI/package-lock.json b/GUI/package-lock.json index d0e72656d..52e002dac 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -8,7 +8,7 @@ "name": "byk-training-module-gui", "version": "0.0.0", "dependencies": { - "@buerokratt-ria/common-gui-components": "^0.0.42", + "@buerokratt-ria/common-gui-components": "^0.0.43", "@buerokratt-ria/header": "^0.1.47", "@buerokratt-ria/menu": "^0.2.10", "@buerokratt-ria/styles": "^0.0.1", @@ -1821,9 +1821,9 @@ } }, "node_modules/@buerokratt-ria/common-gui-components": { - "version": "0.0.42", - "resolved": "https://registry.npmjs.org/@buerokratt-ria/common-gui-components/-/common-gui-components-0.0.42.tgz", - "integrity": "sha512-FOPZBhsbIw3Ii1wi21WzgyAWmaOi6X2/OFE4+bP46tRipe8bsuxK8ZSJ1V5tyWXh6HCCoU2ykXTpdEl9e546hQ==", + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/common-gui-components/-/common-gui-components-0.0.43.tgz", + "integrity": "sha512-xyJyfHo02VARBA7B09vmDxcVZP9a9DFEhm5IADSF5/TX4j8YkzbhEKA4dw1MNhICnnrNrWRo6XAdHDQe84hU2Q==", "peerDependencies": { "@buerokratt-ria/header": "^0.1.20", "@fontsource/roboto": "^4.5.8", diff --git a/GUI/package.json b/GUI/package.json index fff2df425..68d7ba0be 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -14,7 +14,7 @@ "@buerokratt-ria/header": "^0.1.47", "@buerokratt-ria/menu": "^0.2.10", "@buerokratt-ria/styles": "^0.0.1", - "@buerokratt-ria/common-gui-components": "^0.0.42", + "@buerokratt-ria/common-gui-components": "^0.0.43", "@fontsource/roboto": "^4.5.8", "@formkit/auto-animate": "^1.0.0-beta.5", "@radix-ui/react-accessible-icon": "^1.0.1", diff --git a/GUI/src/components/HistoricalChat/Markdownify.tsx b/GUI/src/components/HistoricalChat/Markdownify.tsx index 9e23aaf3c..e55aa56c3 100644 --- a/GUI/src/components/HistoricalChat/Markdownify.tsx +++ b/GUI/src/components/HistoricalChat/Markdownify.tsx @@ -7,25 +7,57 @@ interface MarkdownifyProps { message: string | undefined; } -const LinkPreview: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => { +const isValidImageUrl = (s: string): boolean => { + try { + const u = new URL(s); + if (!/^(https?|data):$/i.test(u.protocol)) return false; + if (s.startsWith('data:image/')) { + return /^data:image\/(png|jpe?g|gif|webp|svg\+xml|bmp);(base64,|charset=utf-8;)/i.test(s); + } + const path = u.pathname.toLowerCase(); + const search = u.search.toLowerCase(); + if (/\.(png|jpe?g|gif|webp|svg|ico|bmp|tiff?|avif|heic|heif|apng)([?#]|$)/i.test(path)) { + return true; + } + if (/\/(images?|img|photos?|pictures?|media|uploads?|thumb|avatar)\//i.test(path)) { + return true; + } + return /[?&](format|type|image|img|photo)=(png|jpe?g|gif|webp|svg|ico)/i.test(search); + } catch { + return false; + } +}; + +const LinkPreview: React.FC<{ + href: string; + children: React.ReactNode; +}> = ({ href, children }) => { const [hasError, setHasError] = useState(false); const basicAuthPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/[^@]+@/; if (basicAuthPattern.test(href)) { return null; } - - return !hasError ? ( + + if (!isValidImageUrl(href)) { + return ( + + {children} + + ); + } + + return hasError ? ( + + {children} + + ) : ( {typeof setHasError(true)} /> - ) : ( - - {children} - ); }; @@ -41,8 +73,14 @@ function formatMessage(message?: string): string { .replaceAll(/\\?\$v\w*/g, '') .replaceAll(/\\?\$g\w*/g, ''); - return filteredMessage - .replaceAll(/&#x([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) + const dataImagePattern = /((?:^|\s))(data:image\/[a-z0-9+]+;[^)\s]+)/gi; + const finalMessage = filteredMessage.replaceAll( + dataImagePattern, + (_, prefix, dataUrl) => `${prefix}[image](${dataUrl})` + ); + + return finalMessage + .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) .replaceAll('&', '&') .replaceAll('>', '>') .replaceAll('<', '<') @@ -50,7 +88,7 @@ function formatMessage(message?: string): string { .replaceAll(''', "'") .replaceAll(''', "'") .replaceAll(/(^|\n)(\d{4})\.\s/g, (match, prefix, year) => { - const remainingText = filteredMessage.substring(filteredMessage.indexOf(match) + match.length); + const remainingText = finalMessage.substring(finalMessage.indexOf(match) + match.length); const sentenceEnd = remainingText.indexOf('\n\n'); if (sentenceEnd !== -1) { const currentSentence = remainingText.substring(0, sentenceEnd); @@ -60,7 +98,7 @@ function formatMessage(message?: string): string { } return `${prefix}${year}\\. `; }) - .replaceAll(/(?<=\n)\d+\.\s/g, hasSpecialFormat(filteredMessage) ? '\n\n$&' : '$&') + .replaceAll(/(?<=\n)\d+\.\s/g, hasSpecialFormat(finalMessage) ? '\n\n$&' : '$&') .replaceAll(/^(\s+)/g, (match) => match.replaceAll(' ', ' ')); }