mirror of
https://github.com/zadam/trilium.git
synced 2026-07-07 15:33:42 +02:00
refactor(llm): use separate component for expandable card
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user