Artifact

O painel ao lado de um chat de IA que mostra o que o modelo criou. Páginas web, SVGs, documentos e código chegam em streaming ao vivo e depois passam para uma prévia isolada em sandbox, com versões que você pode comparar e restaurar, uma divisão redimensionável e um bottom sheet no celular.

Quando um modelo cria algo que vale guardar, como uma página web, um documento ou um arquivo, isso pertence ao lado do chat, e não dentro dele. O Artifact Workspace coloca seu chat e um painel lado a lado. O painel abre por conta própria quando um artifact começa a chegar em streaming, desliza para dentro enquanto o chat abre espaço e pode ser redimensionado, expandido para ocupar toda a largura ou arrastado para fechar. No celular, ele vira um bottom sheet que só abre quando você toca em um card, então a leitura do chat nunca é interrompida.

Enquanto o modelo escreve, o painel mostra código com realce que acompanha as novas linhas, com um botão Jump to latest se você rolar para cima. Páginas web e SVGs passam para a prévia quando a escrita termina, nunca no meio do streaming, e a prévia continua mostrando a última versão concluída até lá. Os documentos são renderizados como Markdown enquanto chegam. As prévias rodam em um iframe em sandbox, sem acesso ao seu site, só trocam depois que a nova versão carregou, sem flash branco, e relatam erros de runtime com uma ação Fix it que você pode enviar de volta ao modelo.

Pequenas mudanças não reescrevem o arquivo. Uma atualização pode enviar edits, cada uma um localizar e substituir aplicado em ordem à última versão concluída, como nos artifacts do Claude. O código permanece na tela e rola até cada edição, as linhas removidas são riscadas e se dobram, o texto novo é digitado com um tom verde e o resto do arquivo fica parado. O card diz Editing com o número de alterações. Uma edição cujo texto não é encontrado, ou é encontrado mais de uma vez, faz aquela versão falhar com uma mensagem dizendo qual edição e por quê, e a última versão boa continua sendo a atual. Use edits para mudanças locais e o content inteiro quando a maior parte do arquivo ou sua estrutura mudar.

Toda atualização é uma nova versão. O cabeçalho as lista com o que mudou, Show changes compara uma versão com a anterior linha a linha com a mesma visão de diff do Diff Review, e versões mais antigas podem ser restauradas sem excluir nada. As contagens de adicionadas e removidas ao lado do alternador e no card são as linhas que o diff mostra. Uma nova versão sempre vem para a frente. Os cards no chat mostram a versão que a mensagem criou, então clicar em um card antigo abre aquela versão. getArtifactsFromMessages monta tudo isso a partir de chamadas de ferramenta do AI SDK, e o painel, a visão de código e a prévia também funcionam isoladamente.

  1. Adicione o registro Pro ao components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Adicione seu token

    Crie um token na sua página de conta e coloque-o em .env.local como HEXTAUI_PRO_TOKEN.

  3. Adicione o bloco

    pnpm dlx shadcn@latest add @hextaui-pro/artifact

Com o AI SDK

getArtifactsFromMessages transforma as chamadas de ferramenta create_artifact e update_artifact do useChat em artifacts com versões. Coloque um ArtifactCard onde cada chamada aparece e restaure adicionando uma chamada de atualização concluída com setMessages. update_artifact roda no navegador, então applyEdits pode avisar o modelo quando uma edição não correspondeu e ele pode tentar de novo.

"use client"

import * as React from "react"
import { useChat } from "@ai-sdk/react"
import { isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls, type UIMessage } from "ai"

import { ChatAssistantMessage, ChatThread, ChatUserMessage } from "../chat-thread/chat-thread"
import { Markdown } from "../markdown/markdown"
import { PromptInput, PromptInputActions, PromptInputBody, PromptInputSubmit, PromptInputTextarea } from "../prompt-input/prompt-input"
import { ArtifactCard, ArtifactWorkspace } from "@/components/blocks/artifact/artifact"
import {
  applyEdits,
  findVersion,
  getArtifactsFromMessages,
  lastCompleteVersion,
  type ArtifactEdit,
} from "@/components/blocks/artifact/artifacts"

function textOf(message: UIMessage) {
  return message.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n\n")
}

export function Assistant() {
  const latest = React.useRef<UIMessage[]>([])
  const { messages, setMessages, sendMessage, status, stop, addToolOutput } = useChat({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
    onToolCall({ toolCall }) {
      if (toolCall.dynamic || toolCall.toolName !== "update_artifact") return
      const input = toolCall.input as { id?: string; content?: string; edits?: ArtifactEdit[] }
      const artifact = getArtifactsFromMessages(latest.current).find((item) => item.id === input.id)
      const earlier = artifact?.versions.filter((version) => version.id !== toolCall.toolCallId) ?? []
      const result =
        input.content === undefined
          ? applyEdits(lastCompleteVersion({ versions: earlier })?.content ?? "", input.edits ?? [])
          : null
      if (result?.error) {
        addToolOutput({ tool: "update_artifact", toolCallId: toolCall.toolCallId, state: "output-error", errorText: result.error })
      } else {
        addToolOutput({ tool: "update_artifact", toolCallId: toolCall.toolCallId, output: { ok: true } })
      }
    },
  })
  React.useLayoutEffect(() => {
    latest.current = messages
  })
  const busy = status === "submitted" || status === "streaming"
  const artifacts = React.useMemo(
    () => getArtifactsFromMessages(messages, { streaming: busy }),
    [messages, busy]
  )

  return (
    <ArtifactWorkspace
      artifacts={artifacts}
      onFix={(artifact, _version, error) =>
        sendMessage({ text: `The preview of ${artifact.title} shows an error: ${error.message}. Please fix it.` })
      }
      onRestore={(artifact, version) =>
        setMessages((current) => [
          ...current,
          {
            id: crypto.randomUUID(),
            role: "assistant",
            parts: [
              {
                type: "tool-update_artifact",
                toolCallId: crypto.randomUUID(),
                state: "output-available",
                input: {
                  id: artifact.id,
                  description: `Restored from version ${artifact.versions.indexOf(version) + 1}`,
                  content: version.content,
                },
                output: null,
              },
            ],
          },
        ])
      }
    >
      <ChatThread
        busy={busy}
        composer={
          <PromptInput status={status} onStop={stop} onSubmit={({ text }) => sendMessage({ text })}>
            <PromptInputBody>
              <PromptInputTextarea />
              <PromptInputActions>
                <PromptInputSubmit />
              </PromptInputActions>
            </PromptInputBody>
          </PromptInput>
        }
      >
        {messages.map((message, index) => {
          if (message.role === "user") {
            return <ChatUserMessage key={message.id} id={message.id} text={textOf(message)} />
          }
          const streaming = busy && index === messages.length - 1
          return (
            <ChatAssistantMessage key={message.id} id={message.id} text={textOf(message)} streaming={streaming}>
              {message.parts.map((part, position) => {
                if (part.type === "text") {
                  return <Markdown key={position} text={part.text} streaming={part.state === "streaming"} />
                }
                if (!isToolUIPart(part)) return null
                const found = findVersion(artifacts, part.toolCallId)
                return found ? (
                  <ArtifactCard key={part.toolCallId} artifactId={found.artifact.id} versionId={part.toolCallId} />
                ) : null
              })}
            </ChatAssistantMessage>
          )
        })}
      </ChatThread>
    </ArtifactWorkspace>
  )
}

As ferramentas no seu servidor

Duas ferramentas bastam: uma cria um artifact com um id estável, a outra o altera. Uma alteração é feita com edits, pares de localizar e substituir para mudanças pequenas, ou com o content inteiro para uma reescrita. Ambas chegam em streaming como entrada da ferramenta, então o painel se preenche, ou a edição é digitada no lugar, enquanto o modelo escreve.

import { streamText, tool, type ModelMessage } from "ai"
import { z } from "zod"

export const artifactTools = {
  create_artifact: tool({
    description:
      "Create a standalone artifact the user will want to keep, edit or run: a web page, an SVG, a document or a code file. Write the complete content.",
    inputSchema: z.object({
      id: z.string().describe("A short, stable kebab-case id, used to update it later"),
      title: z.string(),
      kind: z.enum(["html", "svg", "markdown", "code"]),
      language: z.string().optional().describe("For code, such as ts or python"),
      content: z.string(),
    }),
    execute: async () => ({ ok: true }),
  }),
  update_artifact: tool({
    description: [
      "Change an existing artifact. Send either edits or content, never both.",
      "Use edits for small, local changes: each find is copied exactly from the current version, including whitespace, and must appear in it exactly once. Include a few surrounding lines when a snippet could repeat. Edits apply in order, each to the result of the one before, so a later edit can find text an earlier one wrote.",
      "Use content to rewrite the whole artifact when most of it or its structure changes.",
      "If an edit fails, the result says which one and why. Retry with a longer, unique find, or send the whole content.",
    ].join("\n"),
    inputSchema: z.object({
      id: z.string(),
      description: z.string().describe("What changed, in a few words"),
      edits: z
        .array(
          z.object({
            find: z.string().describe("Text copied exactly from the current version, unique within it"),
            replace: z.string().describe("The new text. Empty to delete."),
          })
        )
        .optional(),
      content: z.string().optional().describe("The whole new content, for a rewrite"),
    }),
  }),
}

export function respond(model: Parameters<typeof streamText>[0]["model"], messages: ModelMessage[]) {
  return streamText({ model, messages, tools: artifactTools })
}

Um painel isolado

Mostre um artifact salvo sem um chat, por exemplo em uma página de compartilhamento. O painel mantém sua própria versão, aba e estado de comparação.

"use client"

import { ArtifactPanel } from "@/components/blocks/artifact/artifact"
import type { Artifact } from "@/components/blocks/artifact/artifacts"

export function SharedArtifact({ artifact }: { artifact: Artifact }) {
  return (
    <div className="h-dvh">
      <ArtifactPanel artifact={artifact} />
    </div>
  )
}

Anatomia

As partes que você compõe, de fora para dentro.

ParteDescrição
ArtifactWorkspaceO layout: seu chat como children, o painel ao lado em telas largas e em um bottom sheet em telas estreitas. Ele decide o que está aberto, abre novos artifacts e anuncia o progresso.
ArtifactCardO card em uma mensagem que abre um artifact, ou uma versão dele, e mostra quando ele está sendo escrito, falhou ou está aberto.
ArtifactPanelO cabeçalho com título, versões, abas e ações, e o código, as mudanças, a prévia ou o documento abaixo dele.
ArtifactCode, ArtifactPreviewA visão de código e diff em streaming, e a prévia em sandbox com seu card de erro.
getArtifactsFromMessages, applyEditsLê chamadas de ferramenta de criação e atualização de mensagens do AI SDK e retorna artifacts com suas versões. applyEdits aplica edições de localizar e substituir da mesma forma, para que você possa conferir uma edição antes de dizer ao modelo que funcionou.
useArtifactWorkspaceAbre e fecha artifacts a partir dos seus próprios controles, por exemplo uma lista de arquivos em uma barra lateral.
PropTipoPadrão
artifactsTodos os artifacts da conversa, em ordem. Geralmente getArtifactsFromMessages(messages).
Artifact[]–
childrenO chat, geralmente um ChatThread.
ReactNode–
openIdO artifact aberto, quando você o controla. Um id que não está em artifacts conta como fechado.
string | null–
defaultOpenIdO artifact aberto inicialmente, quando não controlado.
string | nullnull
onOpenChangeChamado quando um artifact é aberto ou o painel é fechado.
(id: string | null) => void–
autoOpenAbre um artifact em telas largas quando uma versão começa a chegar em streaming. Abre uma vez por versão, nunca depois que você o fecha durante aquele streaming, e nunca move o foco.
booleantrue
defaultPanelSizeA parcela da largura que o painel ocupa ao abrir, em porcentagem. O chat mantém pelo menos 320px e o painel pelo menos 360px. Arrastar o divisor prevalece pelo resto da sessão.
number70
onRestoreMostra Restore nas versões mais antigas. Adiciona o conteúdo antigo como uma nova versão; nada é excluído.
(artifact, version) => void–
onFixMostra Fix it quando a prévia lança um erro. error tem message e line.
(artifact, version, error) => void–
actionsControles extras no cabeçalho, como Publish ou Share.
(artifact) => ReactNode–
PropTipoPadrão
artifactIdO artifact a abrir. Não renderiza nada se ele não existir.
string–
versionIdA versão que este card representa, geralmente o id da chamada de ferramenta. Clicar abre essa versão, e o card diz qual versão ela é.
string–

ArtifactPanel

Renderizado para você dentro do ArtifactWorkspace. Use-o diretamente para mostrar um artifact sem um chat.

PropTipoPadrão
artifactO que mostrar.
Artifact–
versionIdA versão exibida, quando você a controla. null acompanha a versão mais recente.
string | null–
onVersionChangeChamado quando alguém escolhe uma versão, com null para a mais recente.
(versionId: string | null) => void–
onCloseMostra o botão de fechar e fecha com Escape.
() => void–
fullscreen, onFullscreenChangeMostra Expand e Show chat, e sai do modo expandido com Escape.
boolean, (fullscreen: boolean) => void–
onRestore, onFix, actionsO mesmo que no workspace.
see ArtifactWorkspace–

Artifact

Os dados que os componentes leem.

PropTipoPadrão
id, titleUm id estável e o título exibido no cabeçalho e no card.
string–
kindhtml e svg ganham uma prévia em sandbox, markdown é renderizado como documento, code mostra apenas código.
"html" | "svg" | "markdown" | "code"–
language, filenameRealce e o nome do download para código.
string–
versionsDo mais antigo ao mais recente. status é streaming, complete, stopped ou error; note diz o que mudou. edits lista os pares de localizar e substituir que uma atualização direcionada aplicou; content é sempre o resultado completo.
{ id, content, status?, error?, note?, edits?, createdAt?, messageId? }[]–
PropTipoPadrão
messagesMensagens do useChat. Chamadas de ferramenta chamadas create_artifact e update_artifact, ou suas formas em camelCase, viram versões.
UIMessage[]–
options.streamingSe a última mensagem ainda está chegando. Sem isso, chamadas inacabadas contam como interrompidas.
booleanfalse
options.toolsOs nomes das suas próprias ferramentas. Create lê id, title, kind, language, filename, description e content. Update lê id, description e content ou edits.
{ create: string[]; update: string[] }–
update inputcontent reescreve o artifact. edits são aplicadas em ordem à última versão concluída; cada find deve corresponder exatamente uma vez, ou a versão falha com error dizendo qual edição e por quê. Se ambos forem enviados, content prevalece.
{ content: string } | { edits: { find: string; replace: string }[] }–
TeclaAção
EnterEm um card, abre o artifact e move o foco para o título do painel. Em um card aberto, fecha-o.
EscSai do modo expandido, depois fecha o painel e devolve o foco ao card.
←→Redimensiona a divisão enquanto o divisor tem o foco. Abaixo do menor tamanho, o painel fecha ou o chat se oculta.
←→Alterna entre Code e Preview enquanto uma aba tem o foco.
TabPercorre o cabeçalho, a área de código ou documento, que rola com as setas do teclado, e a prévia.
  • O painel é uma região rotulada com um título de verdade. Abri-lo a partir de um card move o foco para esse título, e fechá-lo devolve o foco ao card. Abrir por conta própria enquanto o modelo escreve nunca move o foco, então a digitação no composer nunca é interrompida.
  • Os cards são botões com aria-pressed e aria-controls, e informam se o artifact está sendo escrito, falhou ou foi interrompido. Um status polite anuncia quando a escrita começa e quando uma versão está pronta, em vez de ler cada linha.
  • O iframe de prévia tem como título o nome do artifact. Erros de runtime aparecem como um alerta com a mensagem e a linha, e o mesmo vale para uma edição que não pôde ser aplicada. As mudanças são anunciadas como adicionadas e removidas, e não apenas mostradas em cor.
  • Todo botão de ícone tem um rótulo e um tooltip. Abrir, fechar e expandir deslizam com o chat se reorganizando suavemente, e com movimento reduzido o layout muda de uma vez com um fade curto. As edições aparecem com fade por inteiro em vez de serem digitadas.

Construído com

Os componentes gratuitos do HextaUI de que Artifact é feito. Cada um é instalado separadamente.

Código

12 arquivos, adicionados a components/blocks/artifact.