basic-tool.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { createEffect, createSignal, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js"
  2. import { Collapsible } from "./collapsible"
  3. import type { IconProps } from "./icon"
  4. import { TextShimmer } from "./text-shimmer"
  5. export type TriggerTitle = {
  6. title: string
  7. titleClass?: string
  8. subtitle?: string
  9. subtitleClass?: string
  10. args?: string[]
  11. argsClass?: string
  12. action?: JSX.Element
  13. }
  14. const isTriggerTitle = (val: any): val is TriggerTitle => {
  15. return (
  16. typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
  17. )
  18. }
  19. export interface BasicToolProps {
  20. icon: IconProps["name"]
  21. trigger: TriggerTitle | JSX.Element
  22. children?: JSX.Element
  23. status?: string
  24. hideDetails?: boolean
  25. defaultOpen?: boolean
  26. forceOpen?: boolean
  27. defer?: boolean
  28. locked?: boolean
  29. onSubtitleClick?: () => void
  30. }
  31. export function BasicTool(props: BasicToolProps) {
  32. const [open, setOpen] = createSignal(props.defaultOpen ?? false)
  33. const [ready, setReady] = createSignal(open())
  34. const pending = () => props.status === "pending" || props.status === "running"
  35. let frame: number | undefined
  36. const cancel = () => {
  37. if (frame === undefined) return
  38. cancelAnimationFrame(frame)
  39. frame = undefined
  40. }
  41. onCleanup(cancel)
  42. createEffect(() => {
  43. if (props.forceOpen) setOpen(true)
  44. })
  45. createEffect(
  46. on(
  47. open,
  48. (value) => {
  49. if (!props.defer) return
  50. if (!value) {
  51. cancel()
  52. setReady(false)
  53. return
  54. }
  55. cancel()
  56. frame = requestAnimationFrame(() => {
  57. frame = undefined
  58. if (!open()) return
  59. setReady(true)
  60. })
  61. },
  62. { defer: true },
  63. ),
  64. )
  65. const handleOpenChange = (value: boolean) => {
  66. if (pending()) return
  67. if (props.locked && !value) return
  68. setOpen(value)
  69. }
  70. return (
  71. <Collapsible open={open()} onOpenChange={handleOpenChange} class="tool-collapsible">
  72. <Collapsible.Trigger>
  73. <div data-component="tool-trigger">
  74. <div data-slot="basic-tool-tool-trigger-content">
  75. <div data-slot="basic-tool-tool-info">
  76. <Switch>
  77. <Match when={isTriggerTitle(props.trigger) && props.trigger}>
  78. {(trigger) => (
  79. <div data-slot="basic-tool-tool-info-structured">
  80. <div data-slot="basic-tool-tool-info-main">
  81. <span
  82. data-slot="basic-tool-tool-title"
  83. classList={{
  84. [trigger().titleClass ?? ""]: !!trigger().titleClass,
  85. }}
  86. >
  87. <Show when={pending()} fallback={trigger().title}>
  88. <TextShimmer text={trigger().title} />
  89. </Show>
  90. </span>
  91. <Show when={!pending()}>
  92. <Show when={trigger().subtitle}>
  93. <span
  94. data-slot="basic-tool-tool-subtitle"
  95. classList={{
  96. [trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
  97. clickable: !!props.onSubtitleClick,
  98. }}
  99. onClick={(e) => {
  100. if (props.onSubtitleClick) {
  101. e.stopPropagation()
  102. props.onSubtitleClick()
  103. }
  104. }}
  105. >
  106. {trigger().subtitle}
  107. </span>
  108. </Show>
  109. <Show when={trigger().args?.length}>
  110. <For each={trigger().args}>
  111. {(arg) => (
  112. <span
  113. data-slot="basic-tool-tool-arg"
  114. classList={{
  115. [trigger().argsClass ?? ""]: !!trigger().argsClass,
  116. }}
  117. >
  118. {arg}
  119. </span>
  120. )}
  121. </For>
  122. </Show>
  123. </Show>
  124. </div>
  125. <Show when={!pending() && trigger().action}>{trigger().action}</Show>
  126. </div>
  127. )}
  128. </Match>
  129. <Match when={true}>{props.trigger as JSX.Element}</Match>
  130. </Switch>
  131. </div>
  132. </div>
  133. <Show when={props.children && !props.hideDetails && !props.locked && !pending()}>
  134. <Collapsible.Arrow />
  135. </Show>
  136. </div>
  137. </Collapsible.Trigger>
  138. <Show when={props.children && !props.hideDetails}>
  139. <Collapsible.Content>
  140. <Show when={!props.defer || ready()}>{props.children}</Show>
  141. </Collapsible.Content>
  142. </Show>
  143. </Collapsible>
  144. )
  145. }
  146. export function GenericTool(props: { tool: string; status?: string; hideDetails?: boolean }) {
  147. return <BasicTool icon="mcp" status={props.status} trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
  148. }