Prompt Input

Tek sakin bir satır olarak başlayan, yazdıkça karta dönüşen ve konuşma başladığında aşağı inen bir sohbet yazma alanı. Enter gönderir; Japonca ve Çince girişle de güvenle çalışır. Dosyaları yapıştırın, bırakın ya da seçin; önizleme, ilerleme ve yeniden deneme desteği vardır. @ dosya ekler, / ise imleçte açılan bir menüden komut çalıştırır. Sayı tuşlarıyla çalışan bir model seçici, Max'ta canlanan bir efor kaydırıcısı, bir bağlam halkası, canlı dalga biçimli dikte, araç çipleri, yanıt akarken yazılan mesajlar için bir kuyruk ve yeniden yüklemeden sağ çıkan taslaklar.

Yazma alanı, bir sohbetin insanların en çok dokunduğu kısmıdır; bu yüzden her tuş vuruşu doğru hissettirmelidir. Prompt Input solda + ve sağda gönder ile tek sakin bir satır olarak başlar, metin sarıldığında ya da dosya eklendiğinde karta dönüşür ve temizlenince geri yerleşir.

Enter gönderir ve Shift+Enter yeni satır ekler; Japonca, Çince ve Korece giriş yöntemleri asla kesintiye uğramaz. Dokunmatik ekranlarda klavyede Shift olmadığı için Enter yeni satır ekler. Eklemeler tarayıcının kendi geri alma yığınından geçer, bu yüzden ⌘Z her zaman çalışır; uzun yapıştırmalar önizleyebileceğiniz ya da satır içine geri koyabileceğiniz bir eke dönüşür.

Her şey tek bir bağlamı paylaşan parçalardan oluşturulur. Yalnızca alanı ve gönder düğmesini kullanın ya da ilerleme ve yeniden denemeli dosya yüklemeleri, imleçte @ ve / menüleri, bir model seçici, bir efor kaydırıcısı, bir bağlam halkası, dikte, araç çipleri, yanıt akarken gönderilen mesajlar için bir kuyruk ve yeniden yüklemeden sağ çıkan taslaklar ekleyin.

Prompt Input ücretsizdir. Hesap ya da token olmadan yükleyin ve istediğiniz kadar projede Pro Lisansı kapsamında kullanın.

pnpm dlx shadcn@latest add https://hextaui.com/r/pro/prompt-input.json

AI SDK ile

useChat'in status ve stop değerlerini iletin. Dosyalar data URL'li FileUIPart olarak gelir ve yanıt akarken yazılan mesaj kuyruğa alınır ve yanıt bitince gönderilir.

"use client"

import { useChat } from "@ai-sdk/react"
import type { FileUIPart } from "ai"

import { PromptInputAddMenu } from "@/components/blocks/prompt-input/prompt-controls"
import {
  PromptInput,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputActions,
  PromptInputQueue,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
  type PromptMessage,
} from "@/components/blocks/prompt-input/prompt-input"

function toDataUrl(file: File) {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(String(reader.result))
    reader.onerror = () => reject(reader.error)
    reader.readAsDataURL(file)
  })
}

export function Composer({ chatId }: { chatId: string }) {
  const { sendMessage, status, stop } = useChat({ id: chatId })

  const send = async ({ text, files }: PromptMessage) => {
    const parts: FileUIPart[] = await Promise.all(
      files.map(async ({ file }) => ({
        type: "file" as const,
        mediaType: file.type,
        filename: file.name,
        url: await toDataUrl(file),
      }))
    )
    await sendMessage({ text, files: parts })
  }

  return (
    <PromptInput
      status={status}
      onStop={stop}
      onSubmit={send}
      accept="image/*,.pdf"
      maxFileSize={20 * 1024 * 1024}
      draftKey={chatId}
    >
      <PromptInputQueue />
      <PromptInputBody>
        <PromptInputAttachments />
        <PromptInputTools>
          <PromptInputAddMenu />
        </PromptInputTools>
        <PromptInputTextarea />
        <PromptInputActions>
          <PromptInputSubmit />
        </PromptInputActions>
      </PromptInputBody>
    </PromptInput>
  )
}

Model, efor ve dikte

Model seçiciyi, efor kaydırıcısını ve bağlam halkasını PromptInputMeta ile yazma alanının altına koyun. Araçları + menüsünden açın ve çip olarak gösterin. Kayıtları kendi konuşma modelinize göndermek için transcribe, metin düşer düşmez göndermek için autoSend iletin.

"use client"

import * as React from "react"
import { IconWorld } from "@tabler/icons-react"

import { DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu"

import {
  PromptInputAddMenu,
  PromptInputContextUsage,
  PromptInputDictation,
  PromptInputEffort,
  PromptInputMeta,
  PromptInputModelSelect,
  PromptInputTool,
} from "@/components/blocks/prompt-input/prompt-controls"
import {
  PromptInput,
  PromptInputActions,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
  type PromptMessage,
} from "@/components/blocks/prompt-input/prompt-input"

const models = [
  { value: "large", label: "Large" },
  { value: "small", label: "Small" },
]

const efforts = [
  { value: "low", label: "Low" },
  { value: "medium", label: "Medium" },
  { value: "high", label: "High" },
  { value: "max", label: "Max" },
]

export function Composer({
  onSend,
  contextUsed,
  transcribe,
}: {
  onSend: (message: PromptMessage) => void
  contextUsed: number
  transcribe: (audio: Blob) => Promise<string>
}) {
  const [model, setModel] = React.useState("large")
  const [effort, setEffort] = React.useState("medium")
  const [thinking, setThinking] = React.useState(true)
  const [search, setSearch] = React.useState(false)

  return (
    <PromptInput onSubmit={onSend} focusOnType>
      <PromptInputBody>
        <PromptInputAttachments />
        <PromptInputTools>
          <PromptInputAddMenu>
            <DropdownMenuCheckboxItem
              indicator="end"
              checked={search}
              onCheckedChange={setSearch}
            >
              <IconWorld />
              Web search
            </DropdownMenuCheckboxItem>
          </PromptInputAddMenu>
          {search ? (
            <PromptInputTool
              icon={<IconWorld />}
              tone="blue"
              onRemove={() => setSearch(false)}
            >
              Search
            </PromptInputTool>
          ) : null}
        </PromptInputTools>
        <PromptInputTextarea autoFocus />
        <PromptInputActions>
          <PromptInputDictation transcribe={transcribe} autoSend />
          <PromptInputSubmit />
        </PromptInputActions>
      </PromptInputBody>
      <PromptInputMeta>
        <PromptInputModelSelect
          models={models}
          value={model}
          onValueChange={setModel}
        />
        <PromptInputEffort
          levels={efforts}
          value={effort}
          onValueChange={setEffort}
          recommended="medium"
          thinking={thinking}
          onThinkingChange={setThinking}
        />
        <PromptInputContextUsage used={contextUsed} limit={200000} />
      </PromptInputMeta>
    </PromptInput>
  )
}

Bahsetmeler ve komutlar

Tetikleyici başına bir menü ekleyin. @ bir seçimi metnin üstünde bir çipe çevirir, / ise komutu ekler. Ok tuşları gezinir, Enter veya Tab seçer, Escape kapatır; hepsi metinden ayrılmadan.

"use client"

import { IconBrandTypescript, IconTrash } from "@tabler/icons-react"

import {
  PromptInput,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputActions,
  PromptInputSubmit,
  PromptInputTextarea,
  type PromptMessage,
} from "@/components/blocks/prompt-input/prompt-input"
import { PromptInputMenu, type PromptMenuItem } from "@/components/blocks/prompt-input/prompt-menu"

const files: PromptMenuItem[] = [
  {
    value: "app/page.tsx",
    label: "page.tsx",
    description: "app",
    icon: <IconBrandTypescript />,
  },
  {
    value: "lib/utils.ts",
    label: "utils.ts",
    description: "lib",
    icon: <IconBrandTypescript />,
  },
]

const commands: PromptMenuItem[] = [
  { value: "explain", label: "explain", description: "Explain code step by step" },
  { value: "test", label: "test", description: "Write tests for a file" },
  {
    value: "clear",
    label: "clear",
    description: "Start a new conversation",
    icon: <IconTrash />,
    insert: "",
  },
]

export function Composer({
  onSend,
  onClear,
}: {
  onSend: (message: PromptMessage) => void
  onClear: () => void
}) {
  return (
    <PromptInput onSubmit={onSend}>
      <PromptInputBody>
        <PromptInputAttachments />
        <PromptInputTextarea placeholder="Ask anything, @ to add files, / for commands" />
        <PromptInputActions>
          <PromptInputSubmit />
        </PromptInputActions>
      </PromptInputBody>
      <PromptInputMenu trigger="@" items={files} />
      <PromptInputMenu
        trigger="/"
        items={commands}
        onSelect={(item) => {
          if (item.value === "clear") onClear()
        }}
      />
    </PromptInput>
  )
}

Dosya yükleme

upload'a ilerlemeyi bildiren ve iptal sinyaline uyan bir fonksiyon verin. Sunucu için çok büyük bir dosya gibi, yeniden denemenin düzeltemeyeceği hatalar için retryable: false ile UploadError fırlatın.

"use client"

import {
  PromptInput,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputActions,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
  type PromptMessage,
} from "@/components/blocks/prompt-input/prompt-input"
import { PromptInputAddMenu } from "@/components/blocks/prompt-input/prompt-controls"
import { UploadError, type PromptUpload } from "@/components/blocks/prompt-input/use-prompt-files"

const upload: PromptUpload = (file, { signal, onProgress }) =>
  new Promise((resolve, reject) => {
    const request = new XMLHttpRequest()
    request.open("POST", "/api/upload")
    request.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress((event.loaded / event.total) * 100)
    }
    request.onload = () => {
      if (request.status < 300) {
        resolve(JSON.parse(request.responseText))
        return
      }
      reject(
        new UploadError(
          request.status === 413 ? "Too large for the server" : "Upload failed",
          { retryable: request.status === 429 || request.status >= 500 }
        )
      )
    }
    request.onerror = () => reject(new UploadError("Network error. Try again."))
    signal.addEventListener("abort", () => request.abort())
    const body = new FormData()
    body.append("file", file)
    request.send(body)
  })

export function Composer({ onSend }: { onSend: (message: PromptMessage) => void }) {
  return (
    <PromptInput
      onSubmit={onSend}
      upload={upload}
      accept="image/*,.pdf,.csv"
      maxFiles={5}
      maxFileSize={25 * 1024 * 1024}
      globalDrop
    >
      <PromptInputBody>
        <PromptInputAttachments />
        <PromptInputTools>
          <PromptInputAddMenu />
        </PromptInputTools>
        <PromptInputTextarea />
        <PromptInputActions>
          <PromptInputSubmit />
        </PromptInputActions>
      </PromptInputBody>
    </PromptInput>
  )
}

Ajan harness'i

Gövdeyi PromptInputInset ile sarın ve bir PromptInputFooter ekleyin. Mod, çalıştığı yer ve üzerinde çalıştığı klasör için PromptInputSelect kullanın.

"use client"

import * as React from "react"
import {
  IconCloud,
  IconCode,
  IconDeviceLaptop,
  IconFolder,
  IconFolderOpen,
  IconListCheck,
} from "@tabler/icons-react"

import {
  PromptInputAddMenu,
  PromptInputFooter,
  PromptInputInset,
  PromptInputModel,
  PromptInputSelect,
} from "@/components/blocks/prompt-input/prompt-controls"
import {
  PromptInput,
  PromptInputActions,
  PromptInputAttachments,
  PromptInputBody,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
  type PromptMessage,
} from "@/components/blocks/prompt-input/prompt-input"

const modes = [
  { value: "code", label: "Code", icon: <IconCode /> },
  { value: "plan", label: "Plan", icon: <IconListCheck /> },
]

const locations = [
  { value: "local", label: "Local", icon: <IconDeviceLaptop /> },
  { value: "cloud", label: "Cloud", icon: <IconCloud /> },
]

const folders = [
  { value: "~/code/app", label: "app", icon: <IconFolder /> },
]

const models = [
  { value: "large", label: "Large" },
  { value: "small", label: "Small" },
]

const efforts = [
  { value: "low", label: "Low" },
  { value: "medium", label: "Medium" },
  { value: "high", label: "High" },
]

export function AgentComposer({
  onSend,
}: {
  onSend: (message: PromptMessage & { mode: string }) => void
}) {
  const [mode, setMode] = React.useState("code")
  const [location, setLocation] = React.useState("local")
  const [folder, setFolder] = React.useState<string>()
  const [model, setModel] = React.useState("large")
  const [effort, setEffort] = React.useState("medium")

  return (
    <PromptInput
      layout="expanded"
      onSubmit={(message) => onSend({ ...message, mode })}
    >
      <PromptInputInset>
        <PromptInputBody>
          <PromptInputAttachments />
          <PromptInputTools>
            <PromptInputAddMenu />
            <PromptInputSelect
              label="Mode"
              options={modes}
              value={mode}
              onValueChange={setMode}
              shortcut="alt+m"
            />
          </PromptInputTools>
          <PromptInputTextarea className="min-h-16" />
          <PromptInputActions>
            <PromptInputModel
              models={models}
              model={model}
              onModelChange={setModel}
              levels={efforts}
              effort={effort}
              onEffortChange={setEffort}
            />
            <PromptInputSubmit />
          </PromptInputActions>
        </PromptInputBody>
        <PromptInputFooter>
          <PromptInputSelect
            label="Run on"
            options={locations}
            value={location}
            onValueChange={setLocation}
          />
          <PromptInputSelect
            label="Folder"
            options={folders}
            value={folder}
            onValueChange={setFolder}
            placeholder="Choose folder"
            icon={<IconFolderOpen />}
          />
        </PromptInputFooter>
      </PromptInputInset>
    </PromptInput>
  )
}

Yapı

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

ParçaAçıklama
PromptInputForm ve paylaşılan durum: metin, dosyalar, bahsetmeler, kuyruk ve durum.
PromptInputBodyKompakt ve genişletilmiş arasında değişen yuvarlak yüzey.
PromptInputTextareaAlan. Tüm klavye işleme burada yaşar.
PromptInputToolsSol taraf: + menüsü ve araç çipleri.
PromptInputActionsSağ taraf: dikte ve gönder.
PromptInputSubmitDuruma ve metne göre gönder, durdur veya kuyruğa al.
PromptInputAttachmentsMetnin üstünde dosya ve bahsetme çipleri.
PromptInputMenuTek bir tetikleyici karakter için bir imleç menüsü.
PromptInputMetaModel, efor ve bağlam kontrolleri için yazma alanının altındaki satır.
PromptInputModelModel ve efor için düşünme anahtarıyla birlikte tek bir kontrol.
PromptInputInsetGövdeyi saran, ajan harness'leri için bir alt satır tutan soluk bir kabuk.
PromptInputFooterKonum, klasör ve oturum kontrolleri için iç kabuktaki satır.
PromptInputSelectModlar, çalıştırma konumu, klasörler veya depolar için kompakt bir seçici.
PromptInputQueue, PromptInputSuggestions, PromptInputMeterKuyruktaki mesajlar, başlangıç istemleri ve maxLength yakınında bir karakter ölçeri.

PromptInput

Ayrıca onSubmit ve defaultValue dışındaki her form prop'unu kabul eder.

PropTürVarsayılan
onSubmit{ text, files, mentions } alır. Çözülene kadar metni korumak için bir promise döndürün.
(message: PromptMessage) => void | Promise<unknown>–
statususeChat'in status değerini iletin. Meşgulken gönder, durdur veya kuyruğa al olur.
"ready" | "submitted" | "streaming" | "error""ready"
onStopDurdur düğmesinden ya da Escape ile çağrılır.
() => void–
onEditLastBoş bir alanda ↑ ile yüklemek için son mesajı döndürün.
() => string | void–
valueKontrollü metin.
string–
defaultValueBaşlangıç metni.
string""
onValueChangeMetin değiştikçe çağrılır.
(value: string) => void–
submitOnHangi tuşun gönderdiği. ⌘↵ her zaman gönderir.
"enter" | "mod+enter""enter"
pasteLimitBir yapıştırmanın ek olmasından önceki karakter sayısı.
number | false5000
maxLengthPromptInputMeter tarafından gösterilen kesin limit.
number–
draftKeyGönderilmemiş metni bu anahtar altında yerel depolamaya kaydeder.
string–
globalDropSayfanın herhangi bir yerine bırakılan dosyaları kabul eder.
booleanfalse
focusOnTypeBaşka hiçbir şey odaktayken yazmak, yazmayı buradan başlatır.
booleanfalse
layoutGeçiş yapmak yerine bir düzeni zorlar.
"auto" | "compact" | "expanded""auto"
acceptKabul edilen dosya türleri, input niteliği gibi.
string–
maxFilesAynı anda en fazla dosya.
number–
maxFileSizeBayt cinsinden en büyük dosya.
number–
uploadHer dosyayı yükler. Kalıcı hatalar için retryable: false ile UploadError fırlatın.
(file, { signal, onProgress }) => Promise<unknown>–
onRejectBir limiti aşan ya da yanlış türde dosyalar için çağrılır.
(file: File, reason: string) => void–
disabledTüm yazma alanını devre dışı bırakır.
booleanfalse
PropTürVarsayılan
triggerOnu açan karakter, örneğin @ veya /.
string–
items{ value, label, description?, icon?, hint?, keywords?, insert? }
PromptMenuItem[]–
modeMentions bir çip ekler; text, insert ?? label ekler.
"mention" | "text"–
labelMenünün en üstündeki bölüm etiketi.
string–
limitGösterilen en fazla satır.
number–
onSelectBir seçimden sonra çağrılır.
(item: PromptMenuItem) => void–

PromptInputModel

Model seçici ve efor kaydırıcısı tek kontrolde. Ayrıca value ve onValueChange dışındaki her PromptInputEffort prop'unu alır.

PropTürVarsayılan
modelsİlk dokuzu 1–9 kısayollarını alır.
{ value, label }[]–
modelSeçili model.
string–
onModelChangeSeçimde çağrılır.
(value: string) => void–
levelsEfor düzeyleri, en azdan en çoğa.
{ value, label }[]–
effortSeçili efor.
string–
onEffortChangeKaydırıcı hareket ettikçe çağrılır.
(value: string) => void–
PropTürVarsayılan
modelsİlk dokuzu 1–9 kısayollarını alır.
{ value, label }[]–
valueSeçili model.
string–
onValueChangeSeçimde çağrılır.
(value: string) => void–
PropTürVarsayılan
levelsEn azdan en çoğa.
{ value, label }[]–
valueSeçili düzey.
string–
onValueChangeKaydırıcı hareket ettikçe çağrılır.
(value: string) => void–
recommendedBir düzeyi önerilen olarak işaretler.
string–
thinkingAyarlıysa genişletilmiş düşünme anahtarını gösterir.
boolean–
onThinkingChangeAnahtardan çağrılır.
(thinking: boolean) => void–
thinkingLabelAnahtar etiketi.
string–
descriptionTooltip'teki yardım metni.
string–
PropTürVarsayılan
usedKullanılan token'lar.
number–
limitBağlam penceresi boyutu.
number–
limitsPlan kullanımı gibi ek satırlar.
{ label, used, detail? }[]–
limitsLabelBu satırların başlığı.
string–
PropTürVarsayılan
transcribeKaydı konuşma modelinize gönderir. Verilmezse tarayıcının konuşma tanıması kullanılır.
(audio: Blob) => Promise<string>–
shortcutHer yerden bir kaydı başlatır ve bitirir.
string | false"ctrl+shift+d"
autoSendTranskript düşer düşmez gönderir.
booleanfalse
PropTürVarsayılan
iconÇipte gösterilir ve hover'da × ile değiştirilir.
ReactNode–
childrenAraç adı.
string–
toneÇip rengi.
"blue" | "green" | "amber" | "violet" | "pink""blue"
onRemoveÇipe tıklandığında çağrılır.
() => void–
PropTürVarsayılan
optionsSeçenekler. Açıklamalar her etiketin altında görünür.
{ value, label, icon?, description? }[]–
valueSeçili seçenek. Yer tutucuyu göstermek için ayarlamayın.
string–
onValueChangeSeçimde çağrılır.
(value: string) => void–
labelMode veya Run on gibi erişilebilir ad ve tooltip.
string–
placeholderHiçbir şey seçili değilken gösterilir.
string–
iconSeçili seçeneğin simgesi yoksa gösterilir.
ReactNode–
headingMenünün en üstündeki etiket.
string–
shortcutSeçenekler arasında her yerden, alt+m gibi, döngüyle geçer.
string–
childrenSeçeneklerin altında Open folder gibi ek menü öğeleri.
ReactNode–
TuşAction
EnterGönderir. Dokunmatik ekranlarda bunun yerine bir satır ekler.
ShiftEnterBir satır ekler.
⌘EntersubmitOn mod+enter olsa bile her zaman gönderir.
EscAkan bir yanıtı durdurur, bir menüyü kapatır ya da bir kaydı iptal eder.
↑Boş bir alanda, onEditLast ayarlıysa son mesajınızı geri getirir.
↑↓Açık bir @ veya / menüsünde gezinir.
EnterTabVurgulanan menü satırını seçer.
1–9Model menüsü açıkken bir model seçer.
←→Efor kaydırıcısını hareket ettirir. Home ve End uçlara atlar.
CtrlShiftDDikteyi başlatır ve yeniden basıldığında transkripti ekler.
AltMAjan modu gibi shortcut="alt+m" olan bir PromptInputSelect'i döngüyle değiştirir.
  • Alanın görünür bir yer tutucusu ve erişilebilir bir adı vardır; her simge düğmesinin etiketi ve kısayolunu gösteren bir tooltip'i vardır.
  • İmleç menüsü aria-activedescendant içeren bir combobox'tur; böylece içinde gezinirken odak metinden asla ayrılmaz.
  • Yüklemeler, reddedilenler, kuyruk değişiklikleri ve dikte polite bir live region ile duyurulur.
  • Efor kontrolü değer metni olan bir kaydırıcıdır ve ok tuşları ile Home ve End ile çalışmaya devam eder.
  • Dokunmatik ekranlarda input'lar en az 16px'tir; böylece iOS odaklanınca asla yakınlaştırmaz.
  • Azaltılmış harekette yükseklik değişiklikleri, dalga biçimi ve efor dolgusu animasyonsuz yerleşir.

Şunlarla geliştirildi

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

Kod

10 dosya, components/blocks/prompt-input konumuna eklendi.