Artifact

El panel junto a un chat de IA que muestra lo que creó el modelo. Páginas web, SVG, documentos y código llegan en streaming y luego pasan a una vista previa en sandbox, con versiones que puedes comparar y restaurar, una división redimensionable y una hoja inferior en móviles.

Cuando un modelo crea algo que merece conservarse, como una página web, un documento o un archivo, ese contenido debe estar junto al chat en lugar de dentro. Artifact Workspace coloca tu chat y un panel lado a lado. El panel se abre solo cuando un artifact empieza a transmitirse, se desliza mientras el chat le hace sitio, y se puede redimensionar, expandir a todo el ancho o cerrar arrastrando. En móviles se convierte en una hoja inferior que solo se abre al tocar una tarjeta, así que leer el chat nunca se interrumpe.

Mientras el modelo escribe, el panel muestra código resaltado que sigue las líneas nuevas, con un botón Jump to latest si te desplazas hacia arriba. Las páginas web y los SVG pasan a su vista previa cuando termina la escritura, nunca a mitad del streaming, y la vista previa sigue mostrando la última versión terminada hasta entonces. Los documentos se renderizan como Markdown mientras llegan. Las vistas previas se ejecutan en un iframe en sandbox sin acceso a tu sitio, se intercambian solo después de que la versión nueva haya cargado para que no haya destello blanco, e informan de los errores de ejecución con una acción Fix it que puedes devolver al modelo.

Los cambios pequeños no reescriben el archivo. Una actualización puede enviar edits, cada una un buscar y reemplazar aplicado en orden a la última versión terminada, como los artifacts de Claude. El código permanece en pantalla y se desplaza hasta cada edición, las líneas eliminadas se tachan y se pliegan, el texto nuevo se escribe con un tinte verde y el resto del archivo permanece quieto. La tarjeta dice Editing con el número de cambios. Una edición cuyo texto no se encuentra, o se encuentra más de una vez, hace fallar esa versión con un mensaje que indica qué edición y por qué, y la última versión correcta sigue siendo la actual. Usa edits para cambios locales y el content completo cuando cambie la mayor parte del archivo o su estructura.

Cada actualización es una versión nueva. El encabezado las enumera con lo que cambió, Show changes compara una versión con la anterior línea por línea con la misma vista de diff que Diff Review, y las versiones antiguas se pueden restaurar sin borrar nada. Los contadores de añadidos y eliminados junto al toggle y en la tarjeta son las filas que muestra el diff. Una versión nueva siempre pasa al frente. Las tarjetas del chat muestran la versión que creó su mensaje, así que al hacer clic en una tarjeta antigua se abre esa versión. getArtifactsFromMessages construye todo esto a partir de las llamadas a herramientas de AI SDK, y el panel, la vista de código y la vista previa también funcionan por separado.

  1. Añade el registro Pro a components.json

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

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  3. Añade el bloque

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

Con AI SDK

getArtifactsFromMessages convierte las llamadas a las herramientas create_artifact y update_artifact de useChat en artifacts con versiones. Coloca un ArtifactCard donde aparece cada llamada y restaura añadiendo una llamada de actualización terminada con setMessages. update_artifact se ejecuta en el navegador, así que applyEdits puede avisar al modelo cuando una edición no coincidió y este puede intentarlo de nuevo.

"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>
  )
}

Las herramientas en tu servidor

Bastan dos herramientas: una crea un artifact con un id estable y la otra lo cambia. Un cambio son edits, pares de buscar y reemplazar para cambios pequeños, o el content completo para una reescritura. Ambas llegan en streaming como entrada de la herramienta, así que el panel se llena, o la edición se escribe en su sitio, mientras el modelo escribe.

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 })
}

Un panel por sí solo

Muestra un artifact guardado sin chat, por ejemplo en una página para compartir. El panel conserva su propia versión, pestaña y estado de comparación.

"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>
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
ArtifactWorkspaceEl diseño: tu chat como children, el panel a su lado en pantallas anchas y en una hoja inferior en las estrechas. Decide qué está abierto, abre los artifacts nuevos y anuncia el progreso.
ArtifactCardLa tarjeta de un mensaje que abre un artifact, o una versión suya, y muestra cuándo se está escribiendo, ha fallado o está abierto.
ArtifactPanelEl encabezado con título, versiones, pestañas y acciones, y debajo el código, los cambios, la vista previa o el documento.
ArtifactCode, ArtifactPreviewLa vista de código y diff en streaming, y la vista previa en sandbox con su tarjeta de error.
getArtifactsFromMessages, applyEditsLee las llamadas a herramientas de creación y actualización de los mensajes de AI SDK y devuelve artifacts con sus versiones. applyEdits aplica las ediciones de buscar y reemplazar del mismo modo, para que puedas comprobar una edición antes de decirle al modelo que funcionó.
useArtifactWorkspaceAbre y cierra artifacts desde tus propios controles, por ejemplo una lista de archivos en una barra lateral.
PropTipoPredeterminado
artifactsTodos los artifacts de la conversación, en orden. Normalmente getArtifactsFromMessages(messages).
Artifact[]–
childrenEl chat, normalmente un ChatThread.
ReactNode–
openIdEl artifact abierto, cuando lo controlas. Un id que no está en artifacts cuenta como cerrado.
string | null–
defaultOpenIdEl artifact abierto al principio, cuando no es controlado.
string | nullnull
onOpenChangeSe llama cuando se abre un artifact o se cierra el panel.
(id: string | null) => void–
autoOpenAbre un artifact en pantallas anchas cuando una versión empieza a transmitirse. Se abre una vez por versión, nunca después de que lo cierres durante ese streaming, y nunca mueve el foco.
booleantrue
defaultPanelSizeLa proporción del ancho, en porcentaje, que ocupa el panel al abrirse. El chat conserva al menos 320px y el panel al menos 360px. Arrastrar el divisor prevalece durante el resto de la sesión.
number70
onRestoreMuestra Restore en las versiones antiguas. Añade el contenido antiguo como una versión nueva; no se borra nada.
(artifact, version) => void–
onFixMuestra Fix it cuando la vista previa lanza un error. error tiene message y line.
(artifact, version, error) => void–
actionsControles adicionales del encabezado, como Publish o Share.
(artifact) => ReactNode–
PropTipoPredeterminado
artifactIdEl artifact que se abre. No renderiza nada si no existe.
string–
versionIdLa versión que representa esta tarjeta, normalmente el id de la llamada a la herramienta. Al hacer clic se abre esa versión, y la tarjeta indica de cuál se trata.
string–

ArtifactPanel

Se renderiza por ti dentro de ArtifactWorkspace. Úsalo directamente para mostrar un artifact sin chat.

PropTipoPredeterminado
artifactQué mostrar.
Artifact–
versionIdLa versión mostrada, cuando la controlas. null sigue la versión más reciente.
string | null–
onVersionChangeSe llama cuando alguien elige una versión, con null para la más reciente.
(versionId: string | null) => void–
onCloseMuestra el botón de cierre y cierra con Escape.
() => void–
fullscreen, onFullscreenChangeMuestra Expand y Show chat, y sale del modo expandido con Escape.
boolean, (fullscreen: boolean) => void–
onRestore, onFix, actionsIgual que en el workspace.
see ArtifactWorkspace–

Artifact

Los datos que leen los componentes.

PropTipoPredeterminado
id, titleUn id estable y el título mostrado en el encabezado y la tarjeta.
string–
kindhtml y svg obtienen una vista previa en sandbox, markdown se renderiza como documento y code muestra solo código.
"html" | "svg" | "markdown" | "code"–
language, filenameResaltado y nombre de descarga del código.
string–
versionsDe la más antigua a la más reciente. status es streaming, complete, stopped o error; note indica qué cambió. edits enumera los pares de buscar y reemplazar que aplicó una actualización dirigida; content es siempre el resultado completo.
{ id, content, status?, error?, note?, edits?, createdAt?, messageId? }[]–
PropTipoPredeterminado
messagesMensajes de useChat. Las llamadas a herramientas llamadas create_artifact y update_artifact, o sus formas en camelCase, se convierten en versiones.
UIMessage[]–
options.streamingSi el último mensaje aún está llegando. Sin esto, las llamadas sin terminar cuentan como detenidas.
booleanfalse
options.toolsLos nombres de tus propias herramientas. Create lee id, title, kind, language, filename, description y content. Update lee id, description y content o edits.
{ create: string[]; update: string[] }–
update inputcontent reescribe el artifact. edits se aplican en orden a la última versión terminada; cada find debe coincidir exactamente una vez, o la versión falla con error indicando qué edición y por qué. Si se envían ambos, gana content.
{ content: string } | { edits: { find: string; replace: string }[] }–
KeyAcción
EnterEn una tarjeta, abre su artifact y mueve el foco al título del panel. En una tarjeta abierta, la cierra.
EscSale del modo expandido, luego cierra el panel y devuelve el foco a la tarjeta.
←→Redimensiona la división mientras el divisor tiene el foco. Pasado el tamaño mínimo, el panel se cierra o el chat se oculta.
←→Cambia entre Code y Preview mientras una pestaña tiene el foco.
TabRecorre el encabezado, el área de código o documento, que se desplaza con las teclas de flecha, y la vista previa.
  • El panel es una región etiquetada con un encabezado real. Abrirlo desde una tarjeta mueve el foco a ese encabezado, y cerrarlo devuelve el foco a la tarjeta. Abrirse solo mientras el modelo escribe nunca mueve el foco, así que escribir en el compositor nunca se interrumpe.
  • Las tarjetas son botones con aria-pressed y aria-controls, e indican si el artifact se está escribiendo, ha fallado o se ha detenido. Un estado polite anuncia cuándo empieza la escritura y cuándo hay una versión lista, en lugar de leer cada línea.
  • El iframe de la vista previa se titula con el nombre del artifact. Los errores de ejecución aparecen como una alerta con el mensaje y la línea, y lo mismo ocurre con una edición que no se pudo aplicar. Los cambios se anuncian como añadidos y eliminados, no solo se muestran en color.
  • Cada botón de icono tiene una etiqueta y un tooltip. Abrir, cerrar y expandir se deslizan mientras el chat se reajusta con suavidad, y con movimiento reducido el diseño cambia de golpe con un fundido breve. Las ediciones aparecen con un fundido completo en lugar de escribirse.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Artifact. Cada uno se instala por separado.

Código

12 archivos, añadidos a components/blocks/artifact.