chat.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { FC, Fragment, ReactElement, useCallback } from 'react'
  2. import {
  3. Message,
  4. MessageContent,
  5. Response,
  6. Conversation,
  7. ConversationContent,
  8. ConversationEmptyState,
  9. ConversationScrollButton,
  10. Reasoning,
  11. ReasoningContent,
  12. ReasoningTrigger,
  13. PromptInputProvider,
  14. PromptInputMessage,
  15. Skeleton,
  16. Alert,
  17. } from '../'
  18. import { cn } from '../utils'
  19. import { ShadowRootPortal } from './shadow-root-portal'
  20. import {
  21. DefaultChatTransport,
  22. getToolName,
  23. isFileUIPart,
  24. isReasoningUIPart,
  25. isTextUIPart,
  26. isToolUIPart,
  27. lastAssistantMessageIsCompleteWithToolCalls,
  28. UIMessage,
  29. UIMessagePart,
  30. } from 'ai'
  31. import { useChat, UseChatHelpers } from '@ai-sdk/react'
  32. import { ChatPromptInput } from './chat-prompt-input'
  33. export type ChatRunners = Record<string, (input: any) => any | Promise<any>>
  34. export type ChatRenderers = Record<
  35. string,
  36. (
  37. part: UIMessagePart<any, any>,
  38. chat: UseChatHelpers<UIMessage>
  39. ) => ReactElement
  40. >
  41. export const Chat: FC<{
  42. className: string
  43. chatId: string
  44. api: string
  45. headers: Record<string, string>
  46. runners: ChatRunners
  47. renderers: ChatRenderers
  48. }> = ({ className, chatId, api, headers, runners, renderers }) => {
  49. const chat = useChat({
  50. id: chatId,
  51. transport: new DefaultChatTransport({ api, headers }),
  52. sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
  53. async onToolCall({ toolCall }) {
  54. if (toolCall.dynamic) {
  55. return
  56. }
  57. const run = runners[toolCall.toolName]
  58. if (run) {
  59. const output = run(toolCall.input)
  60. chat.addToolResult({
  61. tool: toolCall.toolName,
  62. toolCallId: toolCall.toolCallId,
  63. output,
  64. })
  65. }
  66. },
  67. })
  68. const handleSubmit = useCallback(
  69. async (message: PromptInputMessage) => {
  70. if (chat.status === 'streaming' || chat.status === 'submitted') {
  71. await chat.stop()
  72. return false
  73. }
  74. if (chat.status !== 'ready') {
  75. return false
  76. }
  77. const text = message.text?.trim()
  78. if (!text || text.length === 0) {
  79. return false
  80. }
  81. chat.sendMessage({ text }) // TODO: files, metadata
  82. return true
  83. },
  84. [chat]
  85. )
  86. return (
  87. <ShadowRootPortal>
  88. <Conversation
  89. className={cn('workbench-conversation', 'h-full', className)}
  90. >
  91. <ConversationContent>
  92. {chat.messages.length === 0 ? (
  93. <ConversationEmptyState />
  94. ) : (
  95. chat.messages.map(message => (
  96. <div key={message.id}>
  97. <Message from={message.role}>
  98. <MessageContent variant="flat">
  99. {message.parts.map((part, i) => {
  100. const key = `${message.role}-${i}`
  101. if (isTextUIPart(part)) {
  102. return <Response key={key}>{part.text}</Response>
  103. }
  104. if (isReasoningUIPart(part)) {
  105. const isStreaming =
  106. chat.status === 'streaming' &&
  107. i === message.parts.length - 1 &&
  108. message.id === chat.messages.at(-1)?.id
  109. return (
  110. <Reasoning
  111. key={key}
  112. className="w-full"
  113. isStreaming={isStreaming}
  114. defaultOpen={false}
  115. >
  116. <ReasoningTrigger />
  117. <ReasoningContent>{part.text}</ReasoningContent>
  118. </Reasoning>
  119. )
  120. }
  121. if (isFileUIPart(part)) {
  122. return <div>{part.filename}</div>
  123. }
  124. if (isToolUIPart(part)) {
  125. switch (part.state) {
  126. case 'input-streaming':
  127. return (
  128. <Skeleton
  129. key={key}
  130. className="h-[40px] w-full rounded-full"
  131. />
  132. )
  133. case 'input-available':
  134. case 'output-available': {
  135. const toolName = getToolName(part)
  136. const render = renderers[toolName]
  137. if (!render) {
  138. // TODO: error message
  139. return null
  140. }
  141. return (
  142. <Fragment key={key}>
  143. {render(part, chat)}
  144. </Fragment>
  145. )
  146. }
  147. case 'output-error':
  148. return <Alert key={key}>{part.errorText}</Alert>
  149. }
  150. }
  151. return null
  152. })}
  153. </MessageContent>
  154. </Message>
  155. </div>
  156. ))
  157. )}
  158. </ConversationContent>
  159. <ConversationScrollButton />
  160. </Conversation>
  161. <PromptInputProvider>
  162. <ChatPromptInput status={chat.status} onSubmit={handleSubmit} />
  163. </PromptInputProvider>
  164. </ShadowRootPortal>
  165. )
  166. }