diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 5a781ab..39bf6af 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -987,6 +987,19 @@ gap: var(--badge-spacing); } +.review-inline-header { + color: var(--secondary); + margin-right: .3em; +} + +.review-inline-feedback { + color: var(--warning); + margin-bottom: .4em; + display: flex; + align-items: center; + gap: var(--badge-spacing); +} + .review-header { display: flex; align-items: center; diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index a6e5f92..3ff1690 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -157,8 +157,10 @@ function _mergeConsecutiveAssistantTexts(msgs) { for (const msg of msgs) { if (msg.type === 'text' && msg.role === 'assistant') { const prev = result[result.length - 1]; + const isReview = msg.content && msg.content.startsWith('[Review:'); + const prevIsReview = prev && prev.content && prev.content.startsWith('[Review:'); if (prev && prev.type === 'text' && prev.role === 'assistant' - && prev.iteration === msg.iteration) { + && prev.iteration === msg.iteration && !isReview && !prevIsReview) { // Gleiche Iteration: nahtlos zusammenfügen (Web-Search-Fragmente etc.) prev.content = (prev.content || '') + ' ' + (msg.content || ''); // IDs/Timestamps: letztes behalten (für Mark-Button etc.) @@ -178,8 +180,9 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) { for (const msg of msgs) { const div = document.createElement('div'); switch (msg.type) { - case 'text': - if (msg.role === 'user') { + case 'text': { + const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback')); + if (msg.role === 'user' && !isReviewMsg) { div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : ''); if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); div.dataset.role = 'user'; @@ -208,6 +211,20 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) { ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind)); div.appendChild(ind); } + } else if (isReviewMsg) { + div.className = 'msg review-bubble markdown' + (msg.isMarked ? ' marked' : ''); + if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); + div.dataset.role = msg.role; + div.dataset.timestamp = msg.createdAt || ''; + if (msg.id) div.dataset.messageId = msg.id; + div.innerHTML = renderMarkdown(msg.content); + parseFluentEmojis(div); + _addTimestamp(div, msg.createdAt); + _addReplyButton(div); + _addDeleteButton(div, msg.id, msg.deletedOn); + _addMarkButton(div, msg.id, msg.isMarked); + _addShareButton(div, msg.id); + fragment.appendChild(div); } else if (msg.role === 'assistant') { div.className = 'msg assistant markdown' + (msg.isMarked ? ' marked' : ''); if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); @@ -224,6 +241,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) { fragment.appendChild(div); } break; + } case 'tool_use': if (!isToolVisible(msg.toolName)) continue; div.className = 'tool-box'; @@ -262,13 +280,21 @@ function renderMessages(msgs, addToConversation) { switch (msg.type) { case 'text': if (msg.role === 'user') { - addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); - if (msg.attachmentCount > 0) { - _autoLoadImageAttachments(msg.id); - addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames); + if (msg.content && msg.content.startsWith('[Review-Feedback')) { + addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); + } else { + addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); + if (msg.attachmentCount > 0) { + _autoLoadImageAttachments(msg.id); + addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames); + } } } else if (msg.role === 'assistant') { - addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); + if (msg.content && msg.content.startsWith('[Review:')) { + addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); + } else { + addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); + } } break; diff --git a/wwwroot/js/pages/shared-page.js b/wwwroot/js/pages/shared-page.js index fdb40bd..3704dcb 100644 --- a/wwwroot/js/pages/shared-page.js +++ b/wwwroot/js/pages/shared-page.js @@ -25,12 +25,18 @@ registerPage('shared', { messages = _mergeConsecutiveTexts(messages); for (const msg of messages) { + const c = msg.content || ''; + const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback'); const div = document.createElement('div'); - div.className = 'msg ' + (msg.role === 'user' ? 'user' : 'assistant') + ' markdown'; - if (msg.role === 'user') { + if (isReview) { + div.className = 'msg review-bubble markdown'; + } else { + div.className = 'msg ' + (msg.role === 'user' ? 'user' : 'assistant') + ' markdown'; + } + if (!isReview && msg.role === 'user') { renderUserContent(div, msg.content); } else { - div.innerHTML = renderMarkdown(msg.content); + div.innerHTML = renderMarkdown(c); parseFluentEmojis(div); wrapKreaGalleries(div); } @@ -56,11 +62,14 @@ registerPage('shared', { function _mergeConsecutiveTexts(msgs) { const result = []; for (const msg of msgs) { - if (msg.role === 'assistant') { + const c = msg.content || ''; + const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback'); + if (msg.role === 'assistant' && !isReview) { const prev = result[result.length - 1]; if (prev && prev.role === 'assistant' + && !(prev.content || '').startsWith('[Review:') && prev.iteration === msg.iteration) { - prev.content = (prev.content || '') + ' ' + (msg.content || ''); + prev.content = (prev.content || '') + ' ' + c; if (msg.createdAt) prev.createdAt = msg.createdAt; continue; } diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 95a7849..41f0fc2 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -167,7 +167,20 @@ function renderMarkdown(text) { return '%%CODE' + (codeBlocks.length - 1) + '%%'; }); - // 4) [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild + // 4a) [Review: AgentName] prefix → Review-Header + html = html.replace(/\[Review:\s*([^\]]+)\]\s*/g, function(match, agentName) { + return ' ' + esc(agentName.trim()) + ' '; + }); + + // 4b) [Review-Feedback von AgentName (Runde X/Y)]: → Review-Feedback-Header + html = html.replace(/\[Review-Feedback von\s+([^\(]+)\(Runde\s+(\d+)\/(\d+)\)\]:\s*/g, function(match, agentName, round, maxRounds) { + return '