tool-error-card.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { type ComponentProps, createMemo, Show, splitProps } from "solid-js"
  2. import { createStore } from "solid-js/store"
  3. import { Card, CardDescription } from "./card"
  4. import { Collapsible } from "./collapsible"
  5. import { Icon } from "./icon"
  6. import { IconButton } from "./icon-button"
  7. import { Tooltip } from "./tooltip"
  8. import { useI18n } from "../context/i18n"
  9. export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "children" | "variant"> {
  10. tool: string
  11. error: string
  12. title?: string
  13. defaultOpen?: boolean
  14. subtitle?: string
  15. href?: string
  16. }
  17. export function ToolErrorCard(props: ToolErrorCardProps) {
  18. const i18n = useI18n()
  19. const [state, setState] = createStore({
  20. open: props.defaultOpen ?? false,
  21. copied: false,
  22. })
  23. const open = () => state.open
  24. const copied = () => state.copied
  25. const [split, rest] = splitProps(props, ["tool", "error", "title", "defaultOpen", "subtitle", "href"])
  26. const name = createMemo(() => {
  27. if (split.title) return split.title
  28. const map: Record<string, string> = {
  29. read: "ui.tool.read",
  30. list: "ui.tool.list",
  31. glob: "ui.tool.glob",
  32. grep: "ui.tool.grep",
  33. task: "ui.tool.task",
  34. webfetch: "ui.tool.webfetch",
  35. websearch: "ui.tool.websearch",
  36. bash: "ui.tool.shell",
  37. apply_patch: "ui.tool.patch",
  38. question: "ui.tool.questions",
  39. }
  40. const key = map[split.tool]
  41. if (!key) return split.tool
  42. if (!key.includes(".")) return key
  43. return i18n.t(key)
  44. })
  45. const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim())
  46. const tail = createMemo(() => {
  47. const value = cleaned()
  48. const prefix = `${split.tool} `
  49. if (value.startsWith(prefix)) return value.slice(prefix.length)
  50. return value
  51. })
  52. const subtitle = createMemo(() => {
  53. if (split.subtitle) return split.subtitle
  54. const parts = tail().split(": ")
  55. if (parts.length <= 1) return i18n.t("ui.toolErrorCard.failed")
  56. const head = (parts[0] ?? "").trim()
  57. if (!head) return i18n.t("ui.toolErrorCard.failed")
  58. return head[0] ? head[0].toUpperCase() + head.slice(1) : i18n.t("ui.toolErrorCard.failed")
  59. })
  60. const body = createMemo(() => {
  61. const parts = tail().split(": ")
  62. if (parts.length <= 1) return cleaned()
  63. return parts.slice(1).join(": ").trim() || cleaned()
  64. })
  65. const copy = async () => {
  66. const text = cleaned()
  67. if (!text) return
  68. await navigator.clipboard.writeText(text)
  69. setState("copied", true)
  70. setTimeout(() => setState("copied", false), 2000)
  71. }
  72. return (
  73. <Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error">
  74. <Collapsible
  75. class="tool-collapsible"
  76. data-open={open() ? "true" : "false"}
  77. open={open()}
  78. onOpenChange={(value) => setState("open", value)}
  79. >
  80. <Collapsible.Trigger>
  81. <div data-component="tool-trigger">
  82. <div data-slot="basic-tool-tool-trigger-content">
  83. <span data-slot="basic-tool-tool-indicator" data-component="tool-error-card-icon">
  84. <Icon name="circle-ban-sign" size="small" style={{ "stroke-width": 1.5 }} />
  85. </span>
  86. <div data-slot="basic-tool-tool-info">
  87. <div data-slot="basic-tool-tool-info-structured">
  88. <div data-slot="basic-tool-tool-info-main">
  89. <span data-slot="basic-tool-tool-title">{name()}</span>
  90. <Show
  91. when={split.href && split.subtitle}
  92. fallback={<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>}
  93. >
  94. <a
  95. data-slot="basic-tool-tool-subtitle"
  96. class="clickable subagent-link"
  97. href={split.href!}
  98. onClick={(e) => e.stopPropagation()}
  99. >
  100. {subtitle()}
  101. </a>
  102. </Show>
  103. </div>
  104. </div>
  105. </div>
  106. </div>
  107. <Collapsible.Arrow />
  108. </div>
  109. </Collapsible.Trigger>
  110. <Collapsible.Content>
  111. <div data-slot="tool-error-card-content">
  112. <Show when={open()}>
  113. <div data-slot="tool-error-card-copy">
  114. <Tooltip
  115. value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
  116. placement="top"
  117. gutter={4}
  118. >
  119. <IconButton
  120. icon={copied() ? "check" : "copy"}
  121. size="normal"
  122. variant="ghost"
  123. onMouseDown={(e) => e.preventDefault()}
  124. onClick={(e) => {
  125. e.stopPropagation()
  126. void copy()
  127. }}
  128. aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
  129. />
  130. </Tooltip>
  131. </div>
  132. </Show>
  133. <Show when={body()}>{(value) => <CardDescription>{value()}</CardDescription>}</Show>
  134. </div>
  135. </Collapsible.Content>
  136. </Collapsible>
  137. </Card>
  138. )
  139. }