From eddd77f97f5514bd754fd0a3b5f027d1c0283a4e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 3 Apr 2026 22:31:58 +0300 Subject: [PATCH] feat(llm): group sources in expandable header --- .../src/translations/en/translation.json | 1 + .../type_widgets/llm_chat/ChatMessage.css | 58 +++++----- .../type_widgets/llm_chat/ChatMessage.tsx | 103 +++++++++++------- .../type_widgets/llm_chat/ToolCallCard.css | 1 + 4 files changed, 91 insertions(+), 72 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 081ec1349b..3d9942e4e4 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1639,6 +1639,7 @@ "web_search": "Web search", "note_tools": "Note access", "sources": "Sources", + "sources_summary": "{{count}} sources from {{sites}} sites", "extended_thinking": "Extended thinking", "legacy_models": "Legacy models", "thinking": "Thinking...", diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.css b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.css index bc5cd05f61..2329f80041 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.css +++ b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.css @@ -70,49 +70,45 @@ 51%, 100% { opacity: 0; } } -/* Citations */ -.llm-chat-citations { - margin-top: 0.75rem; - padding-top: 0.75rem; +/* Citations table (inside a tool-call-card) */ +.llm-chat-citations-list { + width: 100%; + border-collapse: collapse; + font-size: 0.8rem; +} + +.llm-chat-citations-list td { + padding: 0.25rem 0.75rem; +} + +.llm-chat-citations-list tr + tr td { border-top: 1px solid var(--main-border-color); } -.llm-chat-citations-label { - display: flex; - align-items: center; - gap: 0.25rem; - font-size: 0.8rem; - font-weight: 600; - color: var(--muted-text-color); - margin-bottom: 0.25rem; +.llm-chat-citation-title { + max-width: 0; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.llm-chat-citations-list { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-wrap: wrap; - gap: 0.5rem; -} - -.llm-chat-citations-list li { - font-size: 0.8rem; -} - -.llm-chat-citations-list a { +.llm-chat-citation-title a { color: var(--link-color, #007bff); text-decoration: none; - padding: 0.125rem 0.5rem; - background: var(--accented-background-color); - border-radius: 4px; - display: inline-block; } -.llm-chat-citations-list a:hover { +.llm-chat-citation-title a:hover { text-decoration: underline; } +.llm-chat-citation-site { + white-space: nowrap; + color: var(--muted-text-color); + font-size: 0.75rem; + text-align: right; +} + /* Error */ .llm-chat-error { padding: 0.75rem 1rem; diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx index 7180b026fe..276242b24f 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx +++ b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx @@ -3,6 +3,8 @@ import "./ChatMessage.css"; import { Marked } from "marked"; import { useMemo } from "preact/hooks"; +import type { LlmCitation } from "@triliumnext/commons"; + import { t } from "../../../services/i18n.js"; import utils from "../../../services/utils.js"; import { SanitizedHtml } from "../../react/RawHtml.js"; @@ -35,6 +37,65 @@ type ContentGroup = | { type: "text"; block: TextBlock; index: number } | { type: "tool_calls"; blocks: ToolCallBlock[]; index: number }; +function getUniqueSiteCount(citations: LlmCitation[]): number { + const hosts = new Set(); + for (const c of citations) { + if (c.url) { + try { + hosts.add(new URL(c.url).hostname); + } catch { /* ignore invalid URLs */ } + } + } + return hosts.size; +} + +function CitationsSection({ citations }: { citations: LlmCitation[] }) { + const siteCount = getUniqueSiteCount(citations); + const summary = t("llm_chat.sources_summary", { count: citations.length, sites: siteCount }); + + return ( +
+
+ + + {summary} + + + + + {citations.map((citation, idx) => { + const title = citation.title || citation.citedText?.slice(0, 80) || `Source ${idx + 1}`; + let hostname: string | null = null; + if (citation.url) { + try { + hostname = new URL(citation.url).hostname; + } catch { /* ignore */ } + } + + return ( + + + {hostname && ( + + )} + + ); + })} + +
+ {citation.url ? ( + + {title} + + ) : ( + {title} + )} + {hostname}
+
+
+ ); +} + export default function ChatMessage({ message, isStreaming }: Props) { const roleLabel = message.role === "user" ? t("llm_chat.role_user") : t("llm_chat.role_assistant"); const isError = message.type === "error"; @@ -95,47 +156,7 @@ export default function ChatMessage({ message, isStreaming }: Props) { )} {message.citations && message.citations.length > 0 && ( -
-
- - {t("llm_chat.sources")} -
-
    - {message.citations.map((citation, idx) => { - // Determine display text: title, URL hostname, or cited text - let displayText = citation.title; - if (!displayText && citation.url) { - try { - displayText = new URL(citation.url).hostname; - } catch { - displayText = citation.url; - } - } - if (!displayText) { - displayText = citation.citedText?.slice(0, 50) || `Source ${idx + 1}`; - } - - return ( -
  • - {citation.url ? ( - - {displayText} - - ) : ( - - {displayText} - - )} -
  • - ); - })} -
-
+ )}
diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css index e138e1dc2c..2a49577504 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css +++ b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css @@ -4,6 +4,7 @@ border: 1px solid var(--main-border-color); border-radius: 8px; font-size: 0.85rem; + overflow: hidden; } /* Tool call section — individual tool call within a card */