Artifact

Modelin ürettiğini gösteren, yapay zeka sohbetinin yanındaki panel. Web sayfaları, SVG'ler, belgeler ve kod canlı olarak akar, ardından korumalı bir önizlemeye geçer; karşılaştırıp geri yükleyebileceğiniz sürümler, yeniden boyutlandırılabilir bir bölme ve telefonlarda alt sayfa sunar.

Bir model bir web sayfası, belge ya da dosya gibi saklanmaya değer bir şey ürettiğinde, bu sohbetin içinde değil yanında durmalıdır. Artifact Workspace sohbetinizi ve bir paneli yan yana koyar. Panel, bir artifact akmaya başladığında kendiliğinden açılır, sohbet yer açarken kayarak gelir ve yeniden boyutlandırılabilir, tüm genişliği kaplayacak şekilde genişletilebilir ya da sürüklenerek kapatılabilir. Telefonlarda yalnızca bir karta dokunduğunuzda açılan bir alt sayfaya dönüşür; böylece sohbeti okumak hiç kesintiye uğramaz.

Model yazarken panel, yeni satırları izleyen vurgulanmış kodu gösterir; yukarı kaydırırsanız bir Jump to latest düğmesi çıkar. Web sayfaları ve SVG'ler yazma bitince önizlemeye geçer, akış ortasında asla; o zamana kadar önizleme son tamamlanan sürümü göstermeye devam eder. Belgeler akarken Markdown olarak render edilir. Önizlemeler sitenize erişimi olmayan korumalı bir iframe'de çalışır, beyaz bir parlama olmasın diye yalnızca yeni sürüm yüklendikten sonra değiştirilir ve çalışma zamanı hatalarını, modele geri gönderebileceğiniz bir Fix it eylemiyle bildirir.

Küçük değişiklikler dosyayı yeniden yazmaz. Bir güncelleme, tıpkı Claude'un artifact'ları gibi, son tamamlanan sürüme sırayla uygulanan bul ve değiştir çiftleri olan edits gönderebilir. Kod ekranda kalır ve her düzenlemeye kayar, kaldırılan satırlar üstü çizilip katlanır, yeni metin yeşil tonla yazılır ve dosyanın geri kalanı sabit kalır. Kart, değişiklik sayısıyla Editing der. Metni bulunamayan ya da birden fazla bulunan bir düzenleme, hangi düzenleme olduğunu ve nedenini söyleyen bir mesajla o sürümü başarısız kılar ve son iyi sürüm geçerli kalır. Yerel değişiklikler için edits, dosyanın çoğu ya da yapısı değiştiğinde ise tam content kullanın.

Her güncelleme yeni bir sürümdür. Başlık, sürümleri neyin değiştiğiyle listeler; Show changes bir sürümü öncekiyle satır satır, Diff Review ile aynı fark görünümünde karşılaştırır ve eski sürümler hiçbir şey silinmeden geri yüklenebilir. Anahtarın yanındaki ve kart üzerindeki eklenen ve kaldırılan sayıları, farkın gösterdiği satırlardır. Yeni sürüm her zaman öne gelir. Sohbetteki kartlar mesajlarının ürettiği sürümü gösterir; bu yüzden eski bir karta tıklamak o sürümü açar. getArtifactsFromMessages tüm bunları AI SDK araç çağrılarından oluşturur; panel, kod görünümü ve önizleme tek başlarına da çalışır.

  1. Pro registry'yi components.json dosyasına ekleyin

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Token'ınızı ekleyin

    Hesap sayfanızda bir token oluşturun ve .env.local dosyasına HEXTAUI_PRO_TOKEN olarak ekleyin.

  3. Bloğu ekleyin

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

AI SDK ile

getArtifactsFromMessages, useChat'ten gelen create_artifact ve update_artifact araç çağrılarını sürümlü artifact'lara dönüştürür. Her çağrının göründüğü yere bir ArtifactCard koyun ve setMessages ile tamamlanmış bir update çağrısı ekleyerek geri yükleyin. update_artifact tarayıcıda çalışır; böylece applyEdits bir düzenleme eşleşmediğinde modele söyleyebilir ve model yeniden deneyebilir.

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

Sunucunuzdaki araçlar

İki araç yeterlidir: biri kararlı bir id ile artifact oluşturur, diğeri onu değiştirir. Bir değişiklik ya küçük değişiklikler için edits (bul ve değiştir çiftleri) ya da yeniden yazım için tüm content olur. İkisi de araç girdisi olarak akar; böylece model yazarken panel dolar ya da düzenleme yerinde yazılır.

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

Tek başına bir panel

Kaydedilmiş bir artifact'ı sohbet olmadan gösterir; örneğin bir paylaşım sayfasında. Panel kendi sürümünü, sekmesini ve karşılaştırma durumunu korur.

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

Yapı

Dıştan içe doğru birleştirdiğiniz parçalar.

ParçaAçıklama
ArtifactWorkspaceDüzen: sohbetiniz children olarak, panel geniş ekranlarda yanında, dar ekranlarda ise alt sayfada. Neyin açık olduğuna karar verir, yeni artifact'ları açar ve ilerlemeyi duyurur.
ArtifactCardBir mesajdaki, bir artifact'ı ya da bir sürümünü açan ve yazılıyor, başarısız ya da açık olduğunu gösteren kart.
ArtifactPanelBaşlık, sürümler, sekmeler ve eylemlerle üst bilgi; altında kod, değişiklikler, önizleme ya da belge.
ArtifactCode, ArtifactPreviewAkan kod ve fark görünümü ile hata kartı olan korumalı önizleme.
getArtifactsFromMessages, applyEditsAI SDK mesajlarından create ve update araç çağrılarını okur ve artifact'ları sürümleriyle döndürür. applyEdits bul ve değiştir düzenlemelerini aynı şekilde uygular; böylece modele işe yaradığını söylemeden önce bir düzenlemeyi kontrol edebilirsiniz.
useArtifactWorkspaceArtifact'ları kendi kontrollerinizden açar ve kapatır; örneğin bir kenar çubuğundaki dosya listesinden.
PropTürVarsayılan
artifactsKonuşmadaki her artifact, sırayla. Genellikle getArtifactsFromMessages(messages).
Artifact[]–
childrenSohbet, genellikle bir ChatThread.
ReactNode–
openIdSiz kontrol ettiğinizde açık olan artifact. artifacts içinde olmayan bir id kapalı sayılır.
string | null–
defaultOpenIdKontrolsüz olduğunda başlangıçta açık olan artifact.
string | nullnull
onOpenChangeBir artifact açıldığında veya panel kapandığında çağrılır.
(id: string | null) => void–
autoOpenBir sürüm akmaya başladığında geniş ekranlarda bir artifact açar. Sürüm başına bir kez açılır, o akış sırasında kapattıysanız bir daha açılmaz ve odağı asla taşımaz.
booleantrue
defaultPanelSizePanelin açıldığında kapladığı genişlik payı, yüzde olarak. Sohbet en az 320px, panel en az 360px kalır. Ayırıcıyı sürüklemek oturumun geri kalanında önceliklidir.
number70
onRestoreEski sürümlerde Restore gösterir. Eski içeriği yeni bir sürüm olarak ekler; hiçbir şey silinmez.
(artifact, version) => void–
onFixÖnizleme hata fırlattığında Fix it gösterir. error, message ve line içerir.
(artifact, version, error) => void–
actionsPublish veya Share gibi ek başlık kontrolleri.
(artifact) => ReactNode–
PropTürVarsayılan
artifactIdAçılacak artifact. Yoksa hiçbir şey render etmez.
string–
versionIdBu kartın temsil ettiği sürüm, genellikle araç çağrısı id'si. Tıklamak o sürümü açar ve kart hangi sürüm olduğunu söyler.
string–

ArtifactPanel

ArtifactWorkspace içinde sizin için render edilir. Sohbet olmadan bir artifact göstermek için doğrudan kullanın.

PropTürVarsayılan
artifactNe gösterileceği.
Artifact–
versionIdSiz kontrol ettiğinizde gösterilen sürüm. null en son sürümü izler.
string | null–
onVersionChangeBiri bir sürüm seçtiğinde çağrılır; en son sürüm için null.
(versionId: string | null) => void–
onCloseKapatma düğmesini gösterir ve Escape ile kapanır.
() => void–
fullscreen, onFullscreenChangeExpand ve Show chat gösterir, Escape ile genişletilmiş moddan çıkar.
boolean, (fullscreen: boolean) => void–
onRestore, onFix, actionsWorkspace'tekiyle aynı.
see ArtifactWorkspace–

Artifact

Bileşenlerin okuduğu veri.

PropTürVarsayılan
id, titleKararlı bir id ve başlıkta ile kartta gösterilen başlık.
string–
kindhtml ve svg korumalı bir önizleme alır, markdown belge olarak render edilir, code yalnızca kodu gösterir.
"html" | "svg" | "markdown" | "code"–
language, filenameKod için vurgulama ve indirme adı.
string–
versionsEn eskiden başlar. status streaming, complete, stopped veya error olabilir; note neyin değiştiğini söyler. edits, hedefli bir güncellemenin uyguladığı bul ve değiştir çiftlerini listeler; content her zaman tam sonuçtur.
{ id, content, status?, error?, note?, edits?, createdAt?, messageId? }[]–
PropTürVarsayılan
messagesuseChat mesajları. create_artifact ve update_artifact adlı araç çağrıları, ya da bunların camelCase biçimleri, sürümlere dönüşür.
UIMessage[]–
options.streamingSon mesajın hâlâ gelmekte olup olmadığı. Verilmezse bitmemiş çağrılar durdurulmuş sayılır.
booleanfalse
options.toolsKendi araç adlarınız. Create; id, title, kind, language, filename, description ve content okur. Update; id, description ve content ya da edits okur.
{ create: string[]; update: string[] }–
update inputcontent artifact'ı yeniden yazar. edits son tamamlanan sürüme sırayla uygulanır; her find tam olarak bir kez eşleşmelidir, aksi halde sürüm hangi düzenleme olduğunu ve nedenini söyleyen error ile başarısız olur. İkisi de gönderilirse content kazanır.
{ content: string } | { edits: { find: string; replace: string }[] }–
TuşAction
EnterBir kartta artifact'ını açar ve odağı panel başlığına taşır. Açık bir kartta kapatır.
EscGenişletilmiş moddan çıkar, ardından paneli kapatır ve odağı karta döndürür.
←→Ayırıcı odaktayken bölmeyi yeniden boyutlandırır. En küçük boyutun ötesinde panel kapanır ya da sohbet gizlenir.
←→Bir sekme odaktayken Code ve Preview arasında geçiş yapar.
TabBaşlık, ok tuşlarıyla kayan kod veya belge alanı ve önizleme arasında gezinir.
  • Panel, gerçek bir başlığı olan etiketli bir bölgedir. Bir karttan açmak odağı o başlığa taşır, kapatmak odağı karta döndürür. Model yazarken kendiliğinden açılması odağı asla taşımaz; böylece yazma alanında yazmak hiç kesintiye uğramaz.
  • Kartlar aria-pressed ve aria-controls içeren düğmelerdir ve artifact'ın yazılıyor, başarısız ya da durdurulmuş olduğunu söyler. Her satırı okumak yerine, yazma başladığında ve bir sürüm hazır olduğunda polite bir durum duyurusu yapılır.
  • Önizleme iframe'inin başlığı artifact'ın adıdır. Çalışma zamanı hataları mesaj ve satırla birlikte bir alert olarak görünür; uygulanamayan bir düzenleme de öyle. Değişiklikler yalnızca renkle değil, eklendi ve kaldırıldı olarak duyurulur.
  • Her simge düğmesinin bir etiketi ve tooltip'i vardır. Açma, kapama ve genişletme, sohbetin yumuşakça yeniden akmasıyla birlikte kayar; azaltılmış harekette düzen kısa bir solma ile anında değişir. Düzenlemeler yazılmak yerine bütün olarak solarak gelir.

Şunlarla geliştirildi

Artifact bloğunun yapıldığı ücretsiz HextaUI bileşenleri. Her biri tek başına yüklenir.

Kod

12 dosya, components/blocks/artifact konumuna eklendi.