Message.Parts.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { UIMessage as VercelMessage } from '@ai-sdk/react'
  2. import { type DynamicToolUIPart, type ReasoningUIPart, type TextUIPart, type ToolUIPart } from 'ai'
  3. import { BrainIcon, CheckIcon, Loader2 } from 'lucide-react'
  4. import { cn } from 'ui'
  5. import { DisplayBlockRenderer } from './DisplayBlockRenderer'
  6. import { EdgeFunctionRenderer } from './EdgeFunctionRenderer'
  7. import { Tool } from './elements/Tool'
  8. import { useMessageActionsContext, useMessageInfoContext } from './Message.Context'
  9. import {
  10. deployEdgeFunctionInputSchema,
  11. deployEdgeFunctionOutputSchema,
  12. parseExecuteSqlChartResult,
  13. } from './Message.utils'
  14. import { MessageMarkdown } from './MessageMarkdown'
  15. function MessagePartText({ textPart }: { textPart: TextUIPart }) {
  16. const { id, isLoading, readOnly, isUserMessage, state } = useMessageInfoContext()
  17. return (
  18. <MessageMarkdown
  19. id={id}
  20. isLoading={isLoading}
  21. readOnly={readOnly}
  22. className={cn(
  23. 'max-w-none space-y-4 prose prose-sm prose-li:mt-1 [&>div]:my-4 prose-h1:text-xl prose-h1:mt-6 prose-h2:text-lg prose-h2:font-medium prose-h3:no-underline prose-h3:text-base prose-h3:mb-4 prose-strong:font-medium prose-strong:text-foreground prose-ol:space-y-3 prose-ul:space-y-3 prose-li:my-0 wrap-break-word [&>p:not(:last-child)]:mb-2! [&>*>p:first-child]:mt-0! [&>*>p:last-child]:mb-0! [&>*>*>p:first-child]:mt-0! [&>*>*>p:last-child]:mb-0! [&>ol>li]:pl-4!',
  24. isUserMessage && 'text-foreground [&>p]:font-medium',
  25. state === 'editing' && 'animate-pulse'
  26. )}
  27. >
  28. {textPart.text}
  29. </MessageMarkdown>
  30. )
  31. }
  32. function MessagePartDynamicTool({ toolPart }: { toolPart: DynamicToolUIPart }) {
  33. return (
  34. <Tool
  35. icon={
  36. toolPart.state === 'input-streaming' ? (
  37. <Loader2 strokeWidth={1.5} size={12} className="animate-spin" />
  38. ) : (
  39. <CheckIcon strokeWidth={1.5} size={12} className="text-foreground-muted" />
  40. )
  41. }
  42. label={
  43. <div>
  44. {toolPart.state === 'input-streaming' ? 'Running ' : 'Ran '}
  45. <span className="text-foreground-lighter">{`${toolPart.toolName}`}</span>
  46. </div>
  47. }
  48. />
  49. )
  50. }
  51. function MessagePartTool({ toolPart }: { toolPart: ToolUIPart }) {
  52. return (
  53. <Tool
  54. icon={
  55. toolPart.state === 'input-streaming' ? (
  56. <Loader2 strokeWidth={1.5} size={12} className="animate-spin" />
  57. ) : (
  58. <CheckIcon strokeWidth={1.5} size={12} className="text-foreground-muted" />
  59. )
  60. }
  61. label={
  62. <div>
  63. {toolPart.state === 'input-streaming' ? 'Running ' : 'Ran '}
  64. <span className="text-foreground-lighter">{`${toolPart.type.replace('tool-', '')}`}</span>
  65. </div>
  66. }
  67. />
  68. )
  69. }
  70. function MessagePartReasoning({ reasoningPart }: { reasoningPart: ReasoningUIPart }) {
  71. return (
  72. <Tool
  73. icon={
  74. reasoningPart.state === 'streaming' ? (
  75. <Loader2 strokeWidth={1.5} size={12} className="animate-spin" />
  76. ) : (
  77. <BrainIcon strokeWidth={1.5} size={12} className="text-foreground-muted" />
  78. )
  79. }
  80. label={reasoningPart.state === 'streaming' ? 'Thinking...' : 'Reasoned'}
  81. >
  82. {reasoningPart.text}
  83. </Tool>
  84. )
  85. }
  86. function ToolDisplayExecuteSqlLoading({ label = 'Writing SQL...' }: { label?: string }) {
  87. return (
  88. <div className="my-4 rounded-lg border bg-surface-75 heading-meta h-9 px-3 text-foreground-light flex items-center gap-2">
  89. <Loader2 className="w-4 h-4 animate-spin" />
  90. {label}
  91. </div>
  92. )
  93. }
  94. function ToolDisplayExecuteSqlFailure() {
  95. return <div className="text-xs text-danger">Failed to execute SQL.</div>
  96. }
  97. function MessagePartExecuteSql({
  98. toolPart,
  99. isLastPart,
  100. }: {
  101. toolPart: ToolUIPart
  102. isLastPart?: boolean
  103. }) {
  104. const { id, isLastMessage } = useMessageInfoContext()
  105. const { addToolApprovalResponse } = useMessageActionsContext()
  106. const { toolCallId, state, input, output } = toolPart
  107. if (state === 'input-streaming') {
  108. return <ToolDisplayExecuteSqlLoading />
  109. }
  110. if (state === 'output-error') {
  111. return <ToolDisplayExecuteSqlFailure />
  112. }
  113. const { data: chart, success } = parseExecuteSqlChartResult(input)
  114. if (!success) return null
  115. if (
  116. state === 'input-available' ||
  117. state === 'approval-requested' ||
  118. state === 'approval-responded' ||
  119. state === 'output-denied' ||
  120. state === 'output-available'
  121. ) {
  122. const approvalId = state === 'approval-requested' ? toolPart.approval?.id : undefined
  123. return (
  124. <div className="w-auto overflow-x-hidden my-4 space-y-2">
  125. <DisplayBlockRenderer
  126. messageId={id}
  127. toolCallId={toolCallId}
  128. initialArgs={{
  129. sql: chart.sql,
  130. label: chart.label,
  131. isWriteQuery: chart.isWriteQuery,
  132. view: chart.view,
  133. xAxis: chart.xAxis,
  134. yAxis: chart.yAxis,
  135. }}
  136. initialResults={output}
  137. toolState={state}
  138. toolApprovalRespondedApproved={toolPart.approval?.approved}
  139. isLastPart={isLastPart}
  140. isLastMessage={isLastMessage}
  141. onApprove={
  142. approvalId
  143. ? () => addToolApprovalResponse?.({ id: approvalId, approved: true })
  144. : undefined
  145. }
  146. onDeny={
  147. approvalId
  148. ? () => addToolApprovalResponse?.({ id: approvalId, approved: false })
  149. : undefined
  150. }
  151. />
  152. </div>
  153. )
  154. }
  155. return null
  156. }
  157. const TOOL_DEPLOY_EDGE_FUNCTION_STATES_WITH_INPUT = new Set([
  158. 'input-available',
  159. 'approval-requested',
  160. 'approval-responded',
  161. 'output-denied',
  162. 'output-available',
  163. ])
  164. function MessagePartDeployEdgeFunction({ toolPart }: { toolPart: ToolUIPart }) {
  165. const { state, input, output } = toolPart
  166. const { addToolApprovalResponse } = useMessageActionsContext()
  167. if (state === 'input-streaming') {
  168. return (
  169. <div className="my-4 rounded-lg border bg-surface-75 heading-meta h-9 px-3 text-foreground-light flex items-center gap-2">
  170. <Loader2 className="w-4 h-4 animate-spin" />
  171. Writing Edge Function...
  172. </div>
  173. )
  174. }
  175. if (state === 'output-error') {
  176. return <p className="text-xs text-danger">Failed to deploy Edge Function.</p>
  177. }
  178. if (!TOOL_DEPLOY_EDGE_FUNCTION_STATES_WITH_INPUT.has(state)) return null
  179. const parsedInput = deployEdgeFunctionInputSchema.safeParse(input)
  180. if (!parsedInput.success) return null
  181. const parsedOutput = deployEdgeFunctionOutputSchema.safeParse(output)
  182. const isInitiallyDeployed =
  183. state === 'output-available' && parsedOutput.success && parsedOutput.data.success === true
  184. const approvalId = state === 'approval-requested' ? toolPart.approval?.id : undefined
  185. return (
  186. <EdgeFunctionRenderer
  187. label={parsedInput.data.label}
  188. code={parsedInput.data.code}
  189. functionName={parsedInput.data.functionName}
  190. showConfirmFooter={state === 'approval-requested'}
  191. isDeploying={state === 'approval-responded' && toolPart.approval?.approved !== false}
  192. initialIsDeployed={isInitiallyDeployed}
  193. onApprove={
  194. approvalId ? () => addToolApprovalResponse?.({ id: approvalId, approved: true }) : undefined
  195. }
  196. onDeny={
  197. approvalId
  198. ? () => addToolApprovalResponse?.({ id: approvalId, approved: false })
  199. : undefined
  200. }
  201. />
  202. )
  203. }
  204. const MessagePart = {
  205. Text: MessagePartText,
  206. Dynamic: MessagePartDynamicTool,
  207. Tool: MessagePartTool,
  208. Reasoning: MessagePartReasoning,
  209. ExecuteSql: MessagePartExecuteSql,
  210. DeployEdgeFunction: MessagePartDeployEdgeFunction,
  211. } as const
  212. export function MessagePartSwitcher({
  213. part,
  214. isLastPart,
  215. }: {
  216. part: NonNullable<VercelMessage['parts']>[number]
  217. isLastPart?: boolean
  218. }) {
  219. switch (part.type) {
  220. case 'dynamic-tool': {
  221. return <MessagePart.Dynamic toolPart={part} />
  222. }
  223. case 'tool-list_policies':
  224. case 'tool-search_docs':
  225. case 'tool-get_active_incidents':
  226. case 'tool-load_knowledge': {
  227. return <MessagePart.Tool toolPart={part} />
  228. }
  229. case 'reasoning':
  230. return <MessagePart.Reasoning reasoningPart={part} />
  231. case 'text':
  232. return <MessagePart.Text textPart={part} />
  233. case 'tool-execute_sql': {
  234. return <MessagePart.ExecuteSql toolPart={part} isLastPart={isLastPart} />
  235. }
  236. case 'tool-deploy_edge_function': {
  237. return <MessagePart.DeployEdgeFunction toolPart={part} />
  238. }
  239. case 'source-url':
  240. case 'source-document':
  241. case 'file':
  242. default:
  243. return null
  244. }
  245. }