refactor(llm): use separate component for expandable card

This commit is contained in:
Elian Doran
2026-04-03 22:38:11 +03:00
parent bd61af89ae
commit 5441d15654
5 changed files with 146 additions and 101 deletions

View File

@@ -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 (
<div className="llm-chat-tool-call-card">
<details className="llm-chat-tool-call-section">
<summary className="llm-chat-tool-call-section-summary">
<span className="bx bx-link" />
{summary}
<span className="bx bx-chevron-down llm-chat-tool-call-chevron" />
</summary>
<ExpandableCard>
<ExpandableSection icon="bx bx-link" label={summary}>
<table className="llm-chat-citations-list">
<tbody>
{citations.map((citation, idx) => {
@@ -96,8 +92,8 @@ function CitationsSection({ citations }: { citations: LlmCitation[] }) {
})}
</tbody>
</table>
</details>
</div>
</ExpandableSection>
</ExpandableCard>
);
}

View File

@@ -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;
}

View File

@@ -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 (
<details className={`expandable-section ${className ?? ""}`}>
<summary className="expandable-section-summary">
<span className={icon} />
{label}
<span className="bx bx-chevron-down expandable-section-chevron" />
</summary>
<div className="expandable-section-body">
{children}
</div>
</details>
);
}
interface ExpandableCardProps {
children: ComponentChildren;
}
/** A bordered card that groups one or more ExpandableSections. */
export function ExpandableCard({ children }: ExpandableCardProps) {
return (
<div className="expandable-card">
{children}
</div>
);
}

View File

@@ -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);
}

View File

@@ -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 (
<details className={`llm-chat-tool-call-section ${hasError ? "llm-chat-tool-call-error" : ""}`}>
<summary className="llm-chat-tool-call-section-summary">
<span className={toolCallIcon(toolCall)} />
{t(`llm.tools.${toolCall.toolName}`, { defaultValue: toolCall.toolName })}
{detailText && (
<span className="llm-chat-tool-call-detail">{detailText}</span>
)}
{refNoteId && (
<span className="llm-chat-tool-call-note-ref">
{refParentId ? (
<Trans
i18nKey="llm.tools.note_in_parent"
components={{
Note: <NewNoteLink notePath={refNoteId} showNoteIcon noPreview />,
Parent: <NewNoteLink notePath={refParentId} showNoteIcon noPreview />
} as any}
/>
) : (
<NewNoteLink notePath={refNoteId} showNoteIcon noPreview />
)}
</span>
)}
{hasError && <span className="llm-chat-tool-call-error-badge">{t("llm_chat.tool_error")}</span>}
<span className="bx bx-chevron-down llm-chat-tool-call-chevron" />
</summary>
<div className="llm-chat-tool-call-section-body">
<div className="llm-chat-tool-call-input">
<strong>{t("llm_chat.input")}</strong>
<KeyValueTable data={toolCall.input} />
</div>
{toolCall.result && (
<div className={`llm-chat-tool-call-result ${hasError ? "llm-chat-tool-call-result-error" : ""}`}>
<strong>{hasError ? t("llm_chat.error") : t("llm_chat.result")}</strong>
<KeyValueTable data={toolCall.result} />
</div>
)}
<>
{t(`llm.tools.${toolCall.toolName}`, { defaultValue: toolCall.toolName })}
{detailText && (
<span className="llm-chat-tool-call-detail">{detailText}</span>
)}
{refNoteId && (
<span className="llm-chat-tool-call-note-ref">
{refParentId ? (
<Trans
i18nKey="llm.tools.note_in_parent"
components={{
Note: <NewNoteLink notePath={refNoteId} showNoteIcon noPreview />,
Parent: <NewNoteLink notePath={refParentId} showNoteIcon noPreview />
} as any}
/>
) : (
<NewNoteLink notePath={refNoteId} showNoteIcon noPreview />
)}
</span>
)}
{hasError && <span className="llm-chat-tool-call-error-badge">{t("llm_chat.tool_error")}</span>}
</>
);
}
/** A single tool call section within a ToolCallCard. */
function ToolCallSection({ toolCall }: { toolCall: ToolCall }) {
const hasError = toolCall.isError;
return (
<ExpandableSection
icon={toolCallIcon(toolCall)}
label={<ToolCallLabel toolCall={toolCall} />}
className={hasError ? "llm-chat-tool-call-error" : ""}
>
<div className="llm-chat-tool-call-input">
<strong>{t("llm_chat.input")}</strong>
<KeyValueTable data={toolCall.input} />
</div>
</details>
{toolCall.result && (
<div className={`llm-chat-tool-call-result ${hasError ? "llm-chat-tool-call-result-error" : ""}`}>
<strong>{hasError ? t("llm_chat.error") : t("llm_chat.result")}</strong>
<KeyValueTable data={toolCall.result} />
</div>
)}
</ExpandableSection>
);
}
/** A card that groups one or more sequential tool calls together. */
export default function ToolCallCard({ toolCalls }: { toolCalls: ToolCall[] }) {
return (
<div className="llm-chat-tool-call-card">
<ExpandableCard>
{toolCalls.map((tc, idx) => (
<ToolCallSection key={tc.id ?? idx} toolCall={tc} />
))}
</div>
</ExpandableCard>
);
}