diff --git a/src/lib/common/markdown/Markdown.svelte b/src/lib/common/markdown/Markdown.svelte index 477be265..832cdf97 100644 --- a/src/lib/common/markdown/Markdown.svelte +++ b/src/lib/common/markdown/Markdown.svelte @@ -18,7 +18,15 @@ /** @type {boolean} */ rawText = false, /** @type {boolean} */ - scrollable = false + scrollable = false, + /** + * Puts a copy button on every fenced code block. Off by default: on a log + * row or a state dump the control would be chrome over content nobody + * copies, and it is the message surfaces where a snippet is there to be + * taken somewhere else. + * @type {boolean} + */ + copyableCode = false } = $props(); /** @@ -40,13 +48,54 @@ * paragraph that merely mentions a run link in passing gets the treatment too, which is * the right answer — it is still an offer of a live view, wherever it sits. */ - const liveLinkRenderer = new Renderer(); - const renderParagraph = liveLinkRenderer.paragraph.bind(liveLinkRenderer); - liveLinkRenderer.paragraph = (text) => { - if (!liveRunIdInText(text)) return renderParagraph(text); + /** + * Wraps a fenced code block so it can carry a copy button. + * + * Emitted as part of the markdown HTML rather than attached to the DOM + * afterwards, so it survives every re-render of a streaming message without + * anything having to re-scan for new blocks. Nothing from the message is + * interpolated into it — the code is read back out of the DOM when the button + * is clicked, which keeps the copied text identical to what is on screen and + * keeps message content out of generated markup. + * + * @param {string} codeHtml + */ + function withCopyButton(codeHtml) { + return '
' + + '' + + codeHtml + + '
'; + } - return `

${text}

`; - }; + /* Per instance rather than module-level, because the code renderer below + depends on `copyableCode`, which differs by call site. */ + const markdownRenderer = buildRenderer(); + + function buildRenderer() { + const renderer = new Renderer(); + + const renderParagraph = renderer.paragraph.bind(renderer); + renderer.paragraph = (text) => { + if (!liveRunIdInText(text)) return renderParagraph(text); + + return `

${text}

`; + }; + + if (copyableCode) { + const renderCode = renderer.code.bind(renderer); + /** + * @param {string} code + * @param {string | undefined} infostring + * @param {boolean} escaped + */ + renderer.code = (code, infostring, escaped) => withCopyButton(renderCode(code, infostring, escaped)); + } + + return renderer; + } const scrollbarId = `markdown-scrollbar-${uuidv4()}`; const options = { @@ -138,11 +187,84 @@ }; } + /** The button currently showing its "copied" state, if any. */ + /** @type {Element | null} */ + let copiedBtn = null; + /** @type {any} */ + let copyResetTimer = null; + + /** + * @param {Element} btn + * @param {boolean} done + */ + function setCopyState(btn, done) { + const icon = btn.querySelector('i'); + const label = btn.querySelector('.md-code-copy-label'); + btn.classList.toggle('md-code-copy-done', done); + if (icon) icon.className = done ? 'bx bx-check' : 'bx bx-copy'; + if (label) label.textContent = done ? 'Copied!' : 'Copy'; + btn.setAttribute('aria-label', done ? 'Code copied' : 'Copy code'); + } + + /** + * Copies a fenced code block, delegated for the same reason as + * `interceptLinks`: the buttons come from `{@html}`, so Svelte never sees + * those nodes and cannot bind to them. + * + * @param {HTMLElement} node + */ + function interceptCodeCopy(node) { + /** @param {MouseEvent} e */ + const onClick = (e) => { + const btn = /** @type {Element | null} */ (e.target)?.closest?.('.md-code-copy'); + if (!btn) return; + + // The surfaces this renders on put their own click on the block around + // it — collapse a message, jump to its log entry — and reaching for a + // snippet is not a request for either. + e.preventDefault(); + e.stopPropagation(); + + // Read from the DOM, so what lands on the clipboard is exactly what is + // on screen. The trailing newline is `marked`'s, not the author's. + const codeEl = btn.parentElement?.querySelector('pre code') || btn.parentElement?.querySelector('pre'); + const text = (codeEl?.textContent || '').replace(/\n$/, ''); + if (!text) return; + + navigator.clipboard?.writeText(text).then(() => { + if (copiedBtn && copiedBtn !== btn) { + setCopyState(copiedBtn, false); + } + clearTimeout(copyResetTimer); + copiedBtn = btn; + setCopyState(btn, true); + copyResetTimer = setTimeout(() => { + // A streaming re-render can have replaced the button by now. + if (copiedBtn?.isConnected) { + setCopyState(copiedBtn, false); + } + copiedBtn = null; + }, 800); + }).catch(() => { + // Clipboard refused — an insecure context, or permission denied. + // Leave the button alone rather than report a copy that never was. + }); + }; + + node.addEventListener('click', onClick); + return { + destroy() { + clearTimeout(copyResetTimer); + node.removeEventListener('click', onClick); + } + }; + } + let innerText = $derived.by(() => { const normalizedText = typeof text !== 'string' ? `${JSON.stringify(text)}` : text; const markedText = !rawText - ? replaceNewLine(marked(replaceMarkdown(normalizedText || ''), { renderer: liveLinkRenderer })?.toString()) - : marked(normalizedText || '', { breaks: true, renderer: liveLinkRenderer })?.toString(); + ? replaceNewLine(marked(replaceMarkdown(normalizedText || ''), { renderer: markdownRenderer })?.toString()) + : marked(normalizedText || '', { breaks: true, renderer: markdownRenderer })?.toString(); if (!!markedText && markedText.endsWith('
')) { const idx = markedText.lastIndexOf('
'); return markedText.substring(0, idx); @@ -157,6 +279,7 @@ class={`markdown-container markdown-lite ${containerClasses || 'text-white'}`} style={`${containerStyles}`} use:interceptLinks + use:interceptCodeCopy > {@html innerText} + +
+
+ {@render children?.()} +
+
+ + {#if isCollapsible} + + + {/if} + diff --git a/src/lib/styles/app.scss b/src/lib/styles/app.scss index 13c28868..06b6d752 100644 --- a/src/lib/styles/app.scss +++ b/src/lib/styles/app.scss @@ -9,6 +9,8 @@ * Loaded once from src/routes/+layout.svelte alongside the icon CSS. */ +@import "components/collapsible-text"; + @import "pages/auth"; @import "pages/chat"; @import "pages/conversation"; diff --git a/src/lib/styles/components/_collapsible-text.scss b/src/lib/styles/components/_collapsible-text.scss new file mode 100644 index 00000000..956b0d27 --- /dev/null +++ b/src/lib/styles/components/_collapsible-text.scss @@ -0,0 +1,86 @@ +/* ======================================================================== + * src/lib/common/shared/CollapsibleText.svelte + * ======================================================================== + * Shared by the chat bubbles, the persist-log content rows and the + * conversation page's dialog entries, so nothing here may assume a + * particular surface: no background, no padding, no colour of its own + * beyond the toggle. + */ + + +.ctxt { + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 0; + max-width: 100%; +} + + +.ctxt-end { + align-items: flex-end; +} + + +.ctxt-view { + max-width: 100%; +} + + +.ctxt-view-clickable { + cursor: pointer; +} + + +/* The mask is doing the real work. The two things it replaces each had a + surface-specific flaw: a hard cut sliced the chat bubble's rounded corners + flat, and the painted '...' it replaces on the conversation page was drawn + over a hard-coded white gradient, which was wrong the moment the page was + in dark mode. A mask paints nothing, so it is correct on every surface and + in both themes. */ +.ctxt-view-collapsed { + max-height: var(--ctxt-max-height, 16rem); + overflow: hidden; + -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent 100%); + mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent 100%); +} + + +/* Deliberately quiet: it belongs to the content it sits under, not to any row + of actions nearby, so it reads as a hint rather than another button. */ + + +.ctxt-toggle { + display: inline-flex; + align-items: center; + gap: 3px; + margin-top: 2px; + padding: 0.1rem 0.4rem; + background: transparent; + border: 0; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + line-height: 1.6; + color: color-mix(in srgb, var(--color-primary) 80%, transparent); + cursor: pointer; + transition: color 0.15s ease, background-color 0.15s ease; + + + i { + font-size: 13px; + line-height: 1; + } + + + &:hover { + color: var(--color-primary); + background-color: color-mix(in srgb, var(--color-primary) 12%, transparent); + } + + + &:focus-visible { + outline: 2px solid color-mix(in srgb, var(--color-primary) 55%, transparent); + outline-offset: 2px; + } +} diff --git a/src/lib/styles/pages/_agent.scss b/src/lib/styles/pages/_agent.scss index ccfbd4c3..e61ea071 100644 --- a/src/lib/styles/pages/_agent.scss +++ b/src/lib/styles/pages/_agent.scss @@ -336,10 +336,15 @@ $panel-radius: 0.5rem; /* ======================================================================== * src/routes/page/agent/[agentId]/+page.svelte - * ======================================================================== */ + * ======================================================================== + * Prefix is `agd-` (agent detail), NOT `ad-`: ad blockers ship element-hiding + * rules that match a leading `ad-` on a class, which hid this page's layout + * for anyone running one. Keep new classes here clear of `ad`, `ads`, + * `banner`, `sponsor` and `promo` for the same reason. + */ -.ad-page { +.agd-page { display: flex; flex-direction: column; } @@ -350,14 +355,14 @@ $panel-radius: 0.5rem; the cards' min-content is wider than 40% of the page. */ -.ad-grid { +.agd-grid { display: grid; grid-template-columns: 2fr 3fr; gap: 1.125rem; } -.ad-col { +.agd-col { display: flex; flex-direction: column; gap: 0.875rem; @@ -366,26 +371,26 @@ $panel-radius: 0.5rem; @media (max-width: 991.98px) { - .ad-grid { + .agd-grid { grid-template-columns: minmax(0, 1fr); } } /* ===== Section wrapper ===== - Each ad-section hosts a panel card. The card owns its own surface (see the + Each agd-section hosts a panel card. The card owns its own surface (see the panel-shell mixin); this is only the grid slot. No hover lift — the panels are flat and static, and a panel that moves reads as clickable when it is not. */ -.ad-section { +.agd-section { display: block; } @media (max-width: 423px) { - .ad-section { + .agd-section { height: fit-content; } } @@ -399,7 +404,7 @@ $panel-radius: 0.5rem; button's edge lines up with the panels above it. */ -.ad-action-bar { +.agd-action-bar { display: flex; flex-wrap: wrap; align-items: center; @@ -412,7 +417,7 @@ $panel-radius: 0.5rem; /* ===== Buttons ===== */ -.ad-btn { +.agd-btn { display: inline-flex; align-items: center; gap: 0.45rem; @@ -445,7 +450,7 @@ $panel-radius: 0.5rem; } -.ad-btn-primary { +.agd-btn-primary { background-color: var(--color-primary); color: rgb(255 255 255); @@ -456,7 +461,7 @@ $panel-radius: 0.5rem; } -.ad-btn-danger { +.agd-btn-danger { background-color: var(--color-danger); color: rgb(255 255 255); @@ -473,7 +478,7 @@ $panel-radius: 0.5rem; reads as a unified cluster. */ -.ad-btn-ghost { +.agd-btn-ghost { background-color: rgb(255 255 255); border-color: rgb(229 231 235); color: var(--color-primary); diff --git a/src/lib/styles/pages/_chat.scss b/src/lib/styles/pages/_chat.scss index 991f6e6e..1f2a1c60 100644 --- a/src/lib/styles/pages/_chat.scss +++ b/src/lib/styles/pages/_chat.scss @@ -2795,7 +2795,7 @@ /* ===== Popup container ===== */ -.cta-popup { +.cta-overlay { position: absolute; bottom: calc(100% + 6px); left: 0; @@ -2912,7 +2912,7 @@ /* ===== Dark mode ===== */ -.dark .cta-popup { +.dark .cta-overlay { background-color: rgb(31 41 55); border-color: rgb(55 65 81); color: rgb(229 231 235); @@ -3567,6 +3567,10 @@ .cle-meta { position: relative; z-index: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 5px; font-size: 13px; color: color-mix(in srgb, white 68%, transparent); margin-bottom: 8px; @@ -3586,6 +3590,19 @@ } +/* First meta line: agent name on the left, copy button on the right. + The timestamp sits on its own line below. */ + + +.cle-meta-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; +} + + /* H4-equivalent heading inside meta row (replaces Bootstrap .h4) */ @@ -3626,13 +3643,69 @@ } +/* Per-log copy button in the meta row. Sits at low opacity until the log + element is hovered or the button itself is focused, so it never competes + with the log text. */ + + +.cle-copy-btn { + display: inline-flex; + align-items: center; + gap: 4px; + flex: 0 0 auto; + padding: 0.16rem 0.5rem; + font-size: 11px; + font-weight: 600; + line-height: 1; + color: color-mix(in srgb, white 72%, transparent); + background: color-mix(in srgb, black 18%, transparent); + border: 1px solid color-mix(in srgb, var(--color-secondary) 26%, transparent); + border-radius: 999px; + cursor: pointer; + opacity: 0.45; + transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; + + + i { + font-size: 13px; + } + + + &:hover { + color: rgb(255 255 255); + background-color: color-mix(in srgb, var(--color-primary) 20%, transparent); + border-color: color-mix(in srgb, var(--color-primary) 35%, transparent); + } + + + &:focus-visible { + opacity: 1; + outline: 2px solid color-mix(in srgb, var(--color-primary) 55%, transparent); + outline-offset: 2px; + } +} + + +.cle-element:hover .cle-copy-btn, +.cle-copy-btn:focus-visible, +.cle-copy-btn-done { + opacity: 1; +} + + +.cle-copy-btn-done { + color: color-mix(in srgb, var(--color-success, #22c55e) 70%, white); + border-color: color-mix(in srgb, var(--color-success, #22c55e) 40%, transparent); + background: color-mix(in srgb, var(--color-success, #22c55e) 14%, transparent); +} + + /* Timestamp suffix (replaces Bootstrap .ms-2) */ .cle-meta-ts { display: inline-flex; align-items: center; - margin-left: 0.5rem; padding: 0.12rem 0.45rem; font-size: 11px; color: color-mix(in srgb, white 58%, transparent); @@ -3914,58 +3987,6 @@ } -/* Collapsed text clamp (replaces .log-collapse nested under .log-content). - Note: -webkit-line-clamp with display:-webkit-box forces the browser to - resolve overflow-x to 'auto' even if we only set overflow-y:hidden, - which surfaces a native horizontal scrollbar whenever the clamped - content has long lines. Setting overflow:hidden (both axes) clips - cleanly without ever showing a scrollbar in collapsed state. */ - - -.cle-collapse { - overflow: hidden; - height: fit-content; - max-height: 200px; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 10; -} - - -/* Whole collapsible block is a toggle target; the More/Less button stays. */ -.cle-content-clickable { - cursor: pointer; -} - - -/* More/Less toggle (replaces .btn.btn-link.toggle-btn.btn-sm) */ - - -.cle-toggle-btn { - background: color-mix(in srgb, var(--color-primary) 11%, transparent); - border: 1px solid color-mix(in srgb, var(--color-primary) 16%, transparent); - outline: none; - box-shadow: none; - color: color-mix(in srgb, var(--color-primary) 75%, white); - font-size: 12px; - font-weight: 700; - padding: 0.18rem 0.55rem; - margin-top: 8px; - border-radius: 999px; - cursor: pointer; - transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease; - - - &:hover { - color: rgb(255 255 255); - background-color: color-mix(in srgb, var(--color-primary) 20%, transparent); - border-color: color-mix(in srgb, var(--color-primary) 35%, transparent); - transform: translateY(-1px); - text-decoration: none; - } -} - - /* MessageId footer (replaces inline style="margin-top: 10px;") */ @@ -5410,7 +5431,7 @@ } - .cta-popup { + .cta-overlay { bottom: calc(100% + 8px); max-height: 14rem; font-size: 0.75rem; diff --git a/src/lib/styles/pages/_conversation.scss b/src/lib/styles/pages/_conversation.scss index 6a0590a8..783595ac 100644 --- a/src/lib/styles/pages/_conversation.scss +++ b/src/lib/styles/pages/_conversation.scss @@ -261,60 +261,6 @@ } -/* ======================================================================== - * src/routes/page/conversation/[conversationId]/conv-dialog-element.svelte - * ======================================================================== - * Replaces the legacy `.text-collapse` rule from - * src/lib/scss/custom/pages/_conversation.scss. The dialog text is - * collapsed to the first 10 lines by default; clicking the More button - * removes this class so the content can flow to its natural height. - * - * Two clamping mechanisms are layered here for reliability: - * 1. `-webkit-line-clamp: 10` with `display: -webkit-box` gives the - * nicer "line truncation with ellipsis" effect on simple text. - * 2. `max-height: 10lh` is the hard fallback. The inner `` - * wraps its rendered HTML in a `.markdown-container` with - * `overflow-x: auto`, which establishes its own block formatting - * context — that BFC defeats line-clamp on the parent because text - * flow is sealed inside the child. The max-height cap clips - * reliably regardless of inner BFCs and is what actually keeps long - * code blocks / tables collapsed. A `::after` pseudo-element below - * paints a '...' in the bottom-right corner so the truncation is - * still signaled visually when line-clamp's native ellipsis is - * suppressed by the inner BFC. - */ -.text-collapse { - position: relative; - overflow: hidden; - max-height: 10lh; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 10; - line-clamp: 10; -} - -/* Ellipsis fallback for collapsed content whose inner BFC defeats - * -webkit-line-clamp (notably the Markdown container's overflow-x: auto - * around code blocks / tables). The native line-clamp ellipsis already - * handles plain text; this pseudo-element guarantees a '...' indicator - * is rendered even when line-clamp is sealed inside the child BFC. - * - * The gradient fades from transparent to the conversation page - * background (white) so the '...' sits cleanly over the last visible - * line without obscuring readable text. */ -.text-collapse::after { - content: '...'; - position: absolute; - right: 0; - bottom: 0; - padding: 0 0.25rem 0 1.5rem; - background: linear-gradient(to right, rgba(255, 255, 255, 0), rgb(255 255 255) 50%); - color: inherit; - font-weight: inherit; - pointer-events: none; -} - - /* ======================================================================== * src/routes/page/conversation/[conversationId]/conv-dialogs.svelte * ======================================================================== */ diff --git a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index 6edf71cb..3b40e80c 100644 --- a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -66,6 +66,7 @@ import RichContent from './rich-content/rich-content.svelte'; import RcMessage from "./rich-content/rc-message.svelte"; import RcDisclaimer from './rich-content/rc-disclaimer.svelte'; + import CollapsibleText from '$lib/common/shared/CollapsibleText.svelte'; import RcEmbedding from './rich-content/rc-embedding.svelte'; import MessageFileGallery from '$lib/common/files/MessageFileGallery.svelte'; import ChatUtil from './chat-util/chat-util.svelte'; @@ -165,15 +166,16 @@ let scrollbars = $state([]); /** Within this many px of the bottom the thread counts as "at the bottom". */ const BOTTOM_THRESHOLD_PX = 80; - let isPinnedToBottom = $state(true); + /** A bubble's own top + bottom padding, which sits inside its measured height. */ + const BUBBLE_PADDING_PX = 20; /* - * Incoming socket messages never move the viewport on their own. They only keep - * it at the bottom while the user has explicitly asked to follow along — by - * sending a message, or by pressing the jump button (including while a reply is - * still streaming, which is the point of it being clickable in that state). - * Scrolling away from the bottom cancels the follow. + * Whether the thread is following the tail. Incoming socket messages and stream + * chunks move the viewport only while it is already at the bottom, so reading + * back through history is never interrupted; scrolling back down resumes the + * follow, as does the jump button (which works while a reply is still streaming, + * the point of it being clickable in that state). */ - let followStream = $state(false); + let isPinnedToBottom = $state(true); /** @type {import('$conversationTypes').ConversationModel} */ let conversation = $state(/** @type {any} */ (undefined)); @@ -239,7 +241,6 @@ let isListening = $state(false); let isLite = $state(false); let isFrame = $state(false); - let autoScrollLog = $state(false); let loadChatUtils = $state(false); let disableSpeech = $state(false); let isLoading = $state(false); @@ -424,8 +425,11 @@ const top = viewport.scrollTop + target.getBoundingClientRect().top - viewport.getBoundingClientRect().top - 16; viewport.scrollTo({ top, behavior: 'smooth' }); - // A jump to history is a deliberate move away from the tail. - followStream = false; + // A jump into history is a deliberate move away from the tail. The scroll + // listener would work this out on its own once the smooth scroll starts, + // but a message landing in the same frame would read the stale value and + // drag the thread straight back down. + isPinnedToBottom = false; } activeIndexId = messageId; directToLog(messageId); @@ -509,7 +513,8 @@ /** * New messages only pull the thread down while the user is already reading * the bottom of it. Once they scroll up, auto-scroll stops fighting them and - * the "jump to latest" button takes over. + * the "jump to latest" button takes over; scrolling back down to the bottom + * puts the thread on the tail again. */ function trackBottomProximity() { const scrollbar = scrollbars[0]; @@ -519,9 +524,6 @@ const update = () => { const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight; isPinnedToBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX; - if (!isPinnedToBottom) { - followStream = false; - } updateActiveIndex(viewport); }; update(); @@ -752,9 +754,6 @@ * bottom — used by the explicit "jump to latest" button, never by new messages. */ function autoScrollToBottom(force = false) { - if (force) { - followStream = true; - } if (!force && !isPinnedToBottom) return; if (_autoScrollScheduled) return; _autoScrollScheduled = true; @@ -964,12 +963,11 @@ resetProgress(); } - autoScrollLog = true; dialogs.push({ ...message, is_chat_message: true }); - refresh(!followStream); + refresh(); text = ""; } @@ -1000,7 +998,7 @@ isStreaming = false; latestStateLog = message.states; - refresh(!followStream); + refresh(); if (isFrame) { window.parent.postMessage(message, "*"); @@ -1024,7 +1022,7 @@ }); } - refresh(!followStream); + refresh(); if (isFrame) { window.parent.postMessage(message, "*"); @@ -1047,7 +1045,7 @@ } }); } - refresh(!followStream); + refresh(); } @@ -1070,7 +1068,7 @@ } dialogs[dialogs.length - 1].text += message.text; refreshDialogs(); - if (followStream) autoScrollToBottom(); + autoScrollToBottom(); }, 0); } } else { @@ -1106,7 +1104,7 @@ for (const tt of thinkingText) { dialogs[dialogs.length - 1].thought.thinking_text += tt; refreshDialogs(); - if (followStream) autoScrollToBottom(); + autoScrollToBottom(); await delay(10); } } @@ -1114,7 +1112,7 @@ for (const char of item.text) { dialogs[dialogs.length - 1].text += char; refreshDialogs(); - if (followStream) autoScrollToBottom(); + autoScrollToBottom(); await delay(10); } } catch (err) { @@ -1127,7 +1125,7 @@ /** @param {import('$conversationTypes').ChatResponseModel} message */ function afterReceiveLlmStreamMessage(message) { isStreaming = false; - refresh(!followStream); + refresh(); } function stopStreaming() { @@ -2646,23 +2644,31 @@ {:else} - +

{utcToLocal(message.created_at, 'h:mm:ss A')} @@ -2789,7 +2795,16 @@ {:else} - + {@const isLive = message?.message_id === lastBotMsg?.message_id + && message?.uuid === lastBotMsg?.uuid + && (isStreaming || isHandlingQueue || isThinking)} + + + + {/if} @@ -98,7 +98,7 @@ {/if} {#if loadUtils} -

+
{#if children} {@render children()} {/if} diff --git a/src/routes/chat/[agentId]/[conversationId]/persist-log/content-log-element.svelte b/src/routes/chat/[agentId]/[conversationId]/persist-log/content-log-element.svelte index 84d3785d..4529ffcf 100644 --- a/src/routes/chat/[agentId]/[conversationId]/persist-log/content-log-element.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/persist-log/content-log-element.svelte @@ -1,5 +1,6 @@
-
+
{#if data?.agent_id?.length > 0} @@ -109,28 +92,34 @@ {/if} - {`${utcToLocal(data?.created_at, 'hh:mm:ss.SSS A, MMM DD YYYY')} `} +
+ {`${utcToLocal(data?.created_at, 'hh:mm:ss.SSS A, MMM DD YYYY')} `}
- - -
-
+
+ + -
- - {#if isCollapsible} - - {/if} +
{#if data.message_id && data.source === ContentLogSource.UserInput} diff --git a/src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte b/src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte index 931a22ef..ccecda8b 100644 --- a/src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte @@ -1,5 +1,5 @@ -
- {#if dialog?.rich_content?.message?.rich_type === RichType.ProgramCode - && dialog?.rich_content?.message?.language === 'javascript'} - - {:else} - - {/if} -
- -{#if isOverflowing} - -{/if} - + +
+ {#if dialog?.rich_content?.message?.rich_type === RichType.ProgramCode + && dialog?.rich_content?.message?.language === 'javascript'} + + {:else} + + {/if} +
+