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 114198e9a3..bc729af405 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx +++ b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx @@ -8,6 +8,7 @@ import type { LlmCitation } from "@triliumnext/commons"; import { t } from "../../../services/i18n.js"; import utils from "../../../services/utils.js"; import { SanitizedHtml } from "../../react/RawHtml.js"; +import { ExpandableCard, ExpandableSection } from "./ExpandableCard.js"; import { type ContentBlock, getMessageText, type StoredMessage, type TextBlock, type ToolCallBlock } from "./llm_chat_types.js"; import ToolCallCard from "./ToolCallCard.js"; @@ -59,13 +60,8 @@ function CitationsSection({ citations }: { citations: LlmCitation[] }) { const summary = t("llm_chat.sources_summary", { count: citations.length, sites: siteCount }); return ( -
-
- - - {summary} - - + + {citations.map((citation, idx) => { @@ -96,8 +92,8 @@ function CitationsSection({ citations }: { citations: LlmCitation[] }) { })}
-
-
+ + ); } diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.css b/apps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.css new file mode 100644 index 0000000000..94343f5181 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.css @@ -0,0 +1,46 @@ +/* Expandable card — bordered container for collapsible sections */ +.expandable-card { + margin: 0.5rem 0; + border: 1px solid var(--main-border-color); + border-radius: 8px; + font-size: 0.85rem; + overflow: hidden; +} + +/* Expandable section — collapsible details within a card */ +.expandable-section + .expandable-section { + border-top: 1px solid var(--main-border-color); +} + +.expandable-section-summary { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + cursor: pointer; + list-style: none; + font-weight: 500; +} + +.expandable-section-summary::-webkit-details-marker { + display: none; +} + +.expandable-section-summary > .bx { + font-size: 1rem; + margin-right: 0.15rem; +} + +.expandable-section-chevron { + margin-left: auto; + transition: transform 0.2s ease; +} + +.expandable-section[open] .expandable-section-chevron { + transform: rotate(180deg); +} + +.expandable-section-body { + padding: 0; +} diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.tsx b/apps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.tsx new file mode 100644 index 0000000000..999ff3da19 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.tsx @@ -0,0 +1,39 @@ +import "./ExpandableCard.css"; + +import type { ComponentChildren } from "preact"; + +interface ExpandableSectionProps { + icon: string; + label: ComponentChildren; + className?: string; + children: ComponentChildren; +} + +/** A collapsible section within an ExpandableCard. */ +export function ExpandableSection({ icon, label, className, children }: ExpandableSectionProps) { + return ( +
+ + + {label} + + +
+ {children} +
+
+ ); +} + +interface ExpandableCardProps { + children: ComponentChildren; +} + +/** A bordered card that groups one or more ExpandableSections. */ +export function ExpandableCard({ children }: ExpandableCardProps) { + return ( +
+ {children} +
+ ); +} 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 2a49577504..1de4319b8d 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css +++ b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css @@ -1,45 +1,4 @@ -/* Tool call card — groups sequential tool calls */ -.llm-chat-tool-call-card { - margin: 0.5rem 0; - 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 */ -.llm-chat-tool-call-section + .llm-chat-tool-call-section { - border-top: 1px solid var(--main-border-color); -} - -.llm-chat-tool-call-section-summary { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.25rem; - padding: 0.5rem 0.75rem; - cursor: pointer; - list-style: none; - font-weight: 500; -} - -.llm-chat-tool-call-section-summary::-webkit-details-marker { - display: none; -} - -.llm-chat-tool-call-section-summary .llm-chat-tool-call-chevron { - margin-left: auto; - transition: transform 0.2s ease; -} - -.llm-chat-tool-call-section[open] .llm-chat-tool-call-chevron { - transform: rotate(180deg); -} - -.llm-chat-tool-call-section-summary > .bx { - font-size: 1rem; - margin-right: 0.15rem; -} +/* Tool call specific styles (card/section structure is in ExpandableCard.css) */ .llm-chat-tool-call-detail { font-weight: 400; @@ -52,22 +11,18 @@ } /* Section body (input + result) */ -.llm-chat-tool-call-section-body { - padding: 0; -} - -.llm-chat-tool-call-section-body .llm-chat-tool-call-input, -.llm-chat-tool-call-section-body .llm-chat-tool-call-result { +.llm-chat-tool-call-input, +.llm-chat-tool-call-result { padding: 0.5rem 0.75rem; max-height: 300px; overflow: auto; } -.llm-chat-tool-call-section-body .llm-chat-tool-call-result { +.llm-chat-tool-call-result { border-top: 1px solid var(--main-border-color); } -.llm-chat-tool-call-section-body pre { +.expandable-section-body pre { margin: 0; padding: 0.5rem; background: var(--main-background-color); @@ -142,7 +97,7 @@ } /* Tool call error styling */ -.llm-chat-tool-call-error .llm-chat-tool-call-section-summary { +.llm-chat-tool-call-error .expandable-section-summary { color: var(--danger-color, #dc3545); } diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx index 7128ce54f7..aa766fd1b4 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx +++ b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx @@ -4,6 +4,7 @@ import { Trans } from "react-i18next"; import { t } from "../../../services/i18n.js"; import { NewNoteLink } from "../../react/NoteLink.js"; +import { ExpandableCard, ExpandableSection } from "./ExpandableCard.js"; import type { ToolCall } from "./llm_chat_types.js"; interface ToolCallContext { @@ -145,60 +146,68 @@ function KeyValueTable({ data, className, depth = 0 }: { data: unknown; classNam ); } -/** A single tool call section within a ToolCallCard. */ -function ToolCallSection({ toolCall }: { toolCall: ToolCall }) { +/** Build the label content for a tool call section. */ +function ToolCallLabel({ toolCall }: { toolCall: ToolCall }) { const { noteId: refNoteId, parentNoteId: refParentId, detailText } = getToolCallContext(toolCall); const hasError = toolCall.isError; return ( -
- - - {t(`llm.tools.${toolCall.toolName}`, { defaultValue: toolCall.toolName })} - {detailText && ( - {detailText} - )} - {refNoteId && ( - - {refParentId ? ( - , - Parent: - } as any} - /> - ) : ( - - )} - - )} - {hasError && {t("llm_chat.tool_error")}} - - -
-
- {t("llm_chat.input")} - -
- {toolCall.result && ( -
- {hasError ? t("llm_chat.error") : t("llm_chat.result")} - -
- )} + <> + {t(`llm.tools.${toolCall.toolName}`, { defaultValue: toolCall.toolName })} + {detailText && ( + {detailText} + )} + {refNoteId && ( + + {refParentId ? ( + , + Parent: + } as any} + /> + ) : ( + + )} + + )} + {hasError && {t("llm_chat.tool_error")}} + + ); +} + +/** A single tool call section within a ToolCallCard. */ +function ToolCallSection({ toolCall }: { toolCall: ToolCall }) { + const hasError = toolCall.isError; + + return ( + } + className={hasError ? "llm-chat-tool-call-error" : ""} + > +
+ {t("llm_chat.input")} +
-
+ {toolCall.result && ( +
+ {hasError ? t("llm_chat.error") : t("llm_chat.result")} + +
+ )} + ); } /** A card that groups one or more sequential tool calls together. */ export default function ToolCallCard({ toolCalls }: { toolCalls: ToolCall[] }) { return ( -
+ {toolCalls.map((tc, idx) => ( ))} -
+ ); }