HextaUI

Where people see and edit what an AI assistant remembers, grouped by category with the chats it came from. Edits, undo and pause all move smoothly.

Categoría
IA
Componentes usados
20
Archivos
10
Ejemplos de uso
3
Acceso
HextaUI Pro (55 bloques)
Código actualizado
Open in new tab

Parte de HextaUI Pro, los 55 bloques, $49 pago único, Comparar planes

Memory is the page where people see what an assistant remembers about them, fix what it got wrong and take things back. It opens with one summary: whether memory is on, paused or off, how many things the assistant remembers and a meter with one colour per category. Memories are grouped under those categories, each one sentence with the chat it came from, when it was saved and when it was last used. Long memories fold to three lines with Show more, and text in any direction lays out on its own.

Click a memory to edit it in place. The text stays exactly where it was and a field appears around it, with Enter to save and Esc to cancel. Clearing the text asks whether to delete it instead, so an empty memory is never saved. Delete folds the memory away and shows a toast with Undo for five seconds, and onDelete only runs once that window closes, so Undo needs nothing from your server. Select picks several memories for one delete behind a confirmation, and Forget everything asks for a typed word first.

Pause works like a temporary chat for everything. The summary says memory is paused and that nothing was deleted, the list stays visible but quieter, and Resume is the same button. Turning Reference saved memories off says what that means in the same place. Near the limit the meter says so calmly and offers Review least used, which lists the oldest unused memories first.

Every change moves the page smoothly. A memory, a whole category, the add form, a note or an error opens and closes with its height, so everything below slides instead of jumping, and Undo brings a memory back the same way. A memory saved in the background keeps its place when its real id arrives. With reduced motion everything changes at once.

Search covers the memory, its source and its labels. Filter narrows to a category or a project, since a memory can apply to every chat or to one project, and sorts by newest, recently used or least used. Sorting by use shows one list with each memory’s category. While a memory is being edited the list keeps its order, so it never jumps out from under the cursor. Memories that disagree, such as short answers and detailed answers, are flagged with May conflict with and a link to the other one. Long lists render 50 rows at a time and load more as you scroll.

MemoryUpdatedChip is the quiet line a chat shows when the assistant saves something, with the memory and Manage. Drop it into Chat Thread messages when your save_memory tool runs.

  1. Obtén HextaUI Pro

    Un solo pago desbloquea todos los bloques, incluidos los futuros. Ver precios.

  2. 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}"
          }
        }
      }
    }
  3. Añade tu token

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

  4. Añade el bloque

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

Save memories from an AI SDK tool

Give the model a save_memory tool that writes to your database, and render Memory with the same rows. Every change is shown at once and rolls back with a message if your callback throws. Deletes wait out the undo window before onDelete runs.

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

import type { MemoryInput, MemoryRecord } from "@/components/blocks/memory/memory"

type MemoryStore = {
  list: (userId: string) => Promise<MemoryRecord[]>
  add: (userId: string, input: MemoryInput & { source: { label: string; href: string } }) => Promise<MemoryRecord>
  markUsed: (userId: string, ids: string[]) => Promise<void>
  settings: (userId: string) => Promise<{ enabled: boolean; paused: boolean }>
}

function memoryInstructions(memories: MemoryRecord[]) {
  if (memories.length === 0) return ""
  return [
    "What you remember about the user. Use it when it helps, and never mention it unprompted.",
    ...memories.map((memory) => `- [${memory.id}] ${memory.text}`),
  ].join("\n")
}

export async function respond({
  model,
  messages,
  store,
  userId,
  chat,
}: {
  model: Parameters<typeof streamText>[0]["model"]
  messages: ModelMessage[]
  store: MemoryStore
  userId: string
  chat: { id: string; title: string }
}) {
  const { enabled, paused } = await store.settings(userId)
  const active = enabled && !paused
  const memories = active ? await store.list(userId) : []
  if (memories.length > 0) await store.markUsed(userId, memories.map((memory) => memory.id))

  return streamText({
    model,
    messages,
    system: memoryInstructions(memories),
    stopWhen: stepCountIs(5),
    tools: active
      ? {
          save_memory: tool({
            description:
              "Save one durable fact about the user that will help in future chats, such as a preference, their role or an ongoing project. Write it as a short sentence about the user. Don't save secrets, health details or anything they asked you to forget.",
            inputSchema: z.object({
              text: z.string().max(500),
              category: z.enum(["about", "preferences", "work", "projects"]),
            }),
            execute: async ({ text, category }) => {
              const memory = await store.add(userId, {
                text,
                category,
                scope: null,
                source: { label: chat.title, href: `/chat/${chat.id}` },
              })
              return { id: memory.id, text: memory.text }
            },
          }),
        }
      : undefined,
  })
}

Show Memory updated in a chat

When the save_memory tool part appears in a message, render MemoryUpdatedChip inside ChatAssistantMessage. It reads Saving to memory while the call runs and Memory updated when it’s done, and Manage opens Memory with that memory highlighted.

"use client"

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

import { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"

import { ChatAssistantMessage, ChatThread, ChatUserMessage } from "../chat-thread/chat-thread"
import { Markdown } from "../markdown/markdown"
import { PromptInput, PromptInputBody, PromptInputSubmit, PromptInputTextarea } from "../prompt-input/prompt-input"
import { Memory, MemoryUpdatedChip, type MemoryChanges, type MemoryRecord } from "@/components/blocks/memory/memory"

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

async function request<T>(input: string, init?: RequestInit) {
  const response = await fetch(input, { ...init, headers: { "content-type": "application/json" } })
  if (!response.ok) {
    const body = (await response.json().catch(() => ({}))) as { error?: string }
    throw new Error(body.error ?? "Something went wrong.")
  }
  return (response.status === 204 ? null : await response.json()) as T
}

export function ChatWithMemory() {
  const { messages, status, sendMessage } = useChat()
  const [open, setOpen] = React.useState(false)
  const [highlight, setHighlight] = React.useState<string | null>(null)
  const [memories, setMemories] = React.useState<MemoryRecord[]>([])
  const [settings, setSettings] = React.useState({ enabled: true, history: false, paused: false })

  const manage = async (id?: string) => {
    const data = await request<{ memories: MemoryRecord[]; settings: typeof settings }>("/api/memories")
    setMemories(data.memories)
    setSettings(data.settings)
    setHighlight(id ?? null)
    setOpen(true)
  }

  return (
    <>
      <ChatThread
        busy={status === "submitted" || status === "streaming"}
        composer={
          <PromptInput status={status} onSubmit={({ text }) => sendMessage({ text })}>
            <PromptInputBody>
              <PromptInputTextarea />
              <PromptInputSubmit />
            </PromptInputBody>
          </PromptInput>
        }
      >
        {messages.map((message) =>
          message.role === "user" ? (
            <ChatUserMessage key={message.id} id={message.id} text={textOf(message)} />
          ) : (
            <ChatAssistantMessage key={message.id} id={message.id} text={textOf(message)}>
              <Markdown text={textOf(message)} />
              {message.parts.filter(isToolUIPart).flatMap((part) => {
                if (getToolName(part) !== "save_memory") return []
                const input = part.input as { text?: string } | undefined
                const output = part.state === "output-available" ? (part.output as { id: string; text: string }) : null
                return [
                  <MemoryUpdatedChip
                    key={part.toolCallId}
                    memory={output?.text ?? input?.text ?? ""}
                    action="saved"
                    status={part.state === "output-error" ? "error" : output ? "done" : "saving"}
                    onManage={() => void manage(output?.id)}
                  />,
                ]
              })}
            </ChatAssistantMessage>
          )
        )}
      </ChatThread>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent size="lg">
          <DialogHeader>
            <DialogTitle>Memory</DialogTitle>
          </DialogHeader>
          <DialogBody>
            <div className="pb-6">
            <Memory
              memories={memories}
              enabled={settings.enabled}
              historyEnabled={settings.history}
              paused={settings.paused}
              highlight={highlight}
              onToggle={async (setting, value) => {
                await request("/api/memories/settings", { method: "PATCH", body: JSON.stringify({ [setting]: value }) })
                setSettings((current) => ({ ...current, [setting]: value }))
              }}
              onAdd={async (input) => {
                const memory = await request<MemoryRecord>("/api/memories", { method: "POST", body: JSON.stringify(input) })
                setMemories((current) => [memory, ...current])
                return memory
              }}
              onEdit={async (memory, changes: MemoryChanges) => {
                await request(`/api/memories/${memory.id}`, { method: "PATCH", body: JSON.stringify(changes) })
                setMemories((current) => current.map((entry) => (entry.id === memory.id ? { ...entry, ...changes } : entry)))
              }}
              onDelete={async (memory) => {
                await request(`/api/memories/${memory.id}`, { method: "DELETE" })
                setMemories((current) => current.filter((entry) => entry.id !== memory.id))
              }}
              onDeleteMany={async (targets) => {
                const ids = targets.map((entry) => entry.id)
                await request("/api/memories", { method: "DELETE", body: JSON.stringify({ ids }) })
                setMemories((current) => current.filter((entry) => !ids.includes(entry.id)))
              }}
              onForgetAll={async () => {
                await request("/api/memories?all=true", { method: "DELETE" })
                setMemories([])
              }}
            />
            </div>
          </DialogBody>
        </DialogContent>
      </Dialog>
    </>
  )
}

A compact panel in settings

variant="compact" keeps one switch, the storage meter, search and the list, for a settings section or a side panel. Pass now to pin dates on the server and in tests.

"use client"

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

import { SettingsSection, SettingsShell, type SettingsSectionItem } from "../settings/settings"
import { Memory, MemorySkeleton, type MemoryRecord } from "@/components/blocks/memory/memory"

const sections: SettingsSectionItem[] = [
  {
    id: "personalization",
    label: "Personalization",
    description: "What Hexta remembers about you across chats.",
    icon: <IconBookmark />,
  },
]

type Settings = { enabled: boolean; history: boolean; paused: boolean }

export function PersonalizationSettings({
  load,
  save,
}: {
  load: () => Promise<{ memories: MemoryRecord[]; settings: Settings; used: number; limit: number }>
  save: {
    toggle: (settings: Partial<Settings>) => Promise<void>
    edit: (id: string, text: string) => Promise<void>
    remove: (id: string) => Promise<void>
  }
}) {
  const [data, setData] = React.useState<Awaited<ReturnType<typeof load>> | null>(null)
  const [failed, setFailed] = React.useState(false)

  const reload = React.useCallback(() => {
    setFailed(false)
    load()
      .then(setData)
      .catch(() => setFailed(true))
  }, [load])

  React.useEffect(reload, [reload])

  return (
    <SettingsShell sections={sections}>
      <SettingsSection
        id="personalization"
        status={failed ? "error" : data ? "ready" : "loading"}
        skeleton={<MemorySkeleton rows={3} />}
        onRetry={reload}
      >
        {data ? (
          <Memory
            variant="compact"
            memories={data.memories}
            enabled={data.settings.enabled}
            limit={data.limit}
            used={data.used}
            onToggle={(setting, value) => save.toggle({ [setting]: value })}
            onEdit={async (memory, changes) => {
              if (changes.text) await save.edit(memory.id, changes.text)
            }}
            onDelete={async (memory) => {
              await save.remove(memory.id)
              setData((current) =>
                current ? { ...current, memories: current.memories.filter((entry) => entry.id !== memory.id) } : current
              )
            }}
          />
        ) : null}
      </SettingsSection>
    </SettingsShell>
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
MemoryThe whole page: the summary, the switches, search and filters, the add form, the grouped list with selection, and the dialogs.
SummaryWhether memory is on, paused or off, how many things are remembered, the storage meter by category, and the paused, off and nearly full notes.
Category groupA heading with the category’s colour and count, above its own list of memories. Empty categories fold away.
Memory rowThe sentence, then one line with the source chat and date, last used and the project, any conflict, and a menu with Edit, Category, Used in, Open source chat and Delete.
MemoryUpdatedChipThe line inside a chat message when a memory is saved, updated or removed, with Manage.
MemorySkeletonA loading placeholder shaped like the page, for SettingsSection’s skeleton prop.
Forget everything dialogDeletes every memory after the confirm word is typed. It can’t be undone, so it waits for onForgetAll.

Memory

Also accepts every div prop.

PropTipoPredeterminado
memories{ id, text, category?, scope?, source?, createdAt, lastUsedAt?, conflictsWith? }. scope is a project value, or null for every chat. source is { label, href? } for the chat it came from. conflictsWith lists ids of memories that disagree with this one. Dates take a Date, an ISO string or a timestamp.
MemoryRecord[]–
enabledWhether the assistant saves and uses memories. When off, the summary explains it and the list stays as it is.
boolean–
historyEnabledWhether the assistant may look back at past chats.
booleanfalse
pausedPauses memory. The summary says so and that nothing was deleted, and Pause becomes Resume.
booleanfalse
onToggleCalled when a switch, Pause or Resume changes. The control moves right away and goes back if this throws.
(setting: "enabled" | "history" | "paused", value: boolean) => void | Promise<void>–
onAddAdds Add memory. The new memory shows as Saving until you return the saved record. Throw to show Try again and Discard.
(input: { text, category?, scope? }) => Promise<MemoryRecord>–
onEditCalled for inline edits and for Category and Used in in the row menu. Without it memories can’t be edited.
(memory, changes: { text?, category?, scope? }) => void | Promise<void>–
onDeleteDelete the memory. Runs after the undo window. Throw and it comes back with a message.
(memory) => void | Promise<void>–
onDeleteManyDelete a selection in one call. Without it, onDelete runs for each one.
(memories) => void | Promise<void>–
onForgetAllAdds Forget everything. The dialog waits for it and keeps everything if it throws.
() => void | Promise<void>–
limitHow much memory holds, in the same unit as used. At 80% the summary says it’s nearly full and at 100% Add memory turns off.
number4000
usedHow much is used, if your server counts tokens. Defaults to the characters in every memory.
number–
categoriesThe categories memories are grouped by, in this order. tone is chart-1 to chart-5 and colours the group’s dot and its part of the meter.
{ value, label, tone? }[]About you, Preferences, Work, Projects
projectsProjects a memory can be scoped to. Adds Used in to the filter, the row menu and the add form.
{ value, label }[][]
highlightA memory id to scroll to, focus and briefly highlight, for example after Manage in a chat. Clears search and filters so it’s visible.
string | null–
variantcompact keeps one switch, the meter, search and the list, without selection or Forget everything.
"full" | "compact""full"
assistantThe assistant’s name in every sentence.
string"Hexta"
undoTimeoutHow long Undo stays, in milliseconds. 0 deletes right away.
number5000
pageSizeRows rendered at a time. More load as the end of the list scrolls into view.
number50
confirmWordThe word typed to confirm Forget everything.
string"forget"
nowPins the clock for dates, for tests and server rendering.
Date–

MemoryUpdatedChip

Also accepts every div prop.

PropTipoPredeterminado
memoryThe memory, shown on one line with the full text on hover.
{ text } | string–
actionSaved to memory, Memory updated or Removed from memory.
"saved" | "updated" | "deleted""updated"
statussaving shimmers while the tool runs. error shows the message and Try again.
"saving" | "done" | "error""done"
errorReplaces Couldn’t save to memory.
string–
onManageAdds Manage, usually opening Memory with highlight set to this memory.
() => void–
onRetryAdds Try again when status is error.
() => void–
KeyAcción
EnterOn a memory, starts editing it. While editing, saves.
EscCancels the edit, clears the search or leaves Select.
F2Edits the memory whose row has focus.
DeleteDeletes the memory whose row has focus, with Undo. Backspace works too.
⇧EnterAdds a line break while editing or adding.
  • Each category is its own list, named by its heading and aware of its full length, so screen readers say “3 of 12” even while only part of it is rendered.
  • Saving, deleting, restoring, pausing and failures are announced through a polite live region, such as “Memory deleted. Undo is available for a few seconds.”
  • Focus never gets lost: after a delete it moves to the next memory, Undo puts it back on the restored one, and Manage focuses the highlighted memory. A row on its way out is inert.
  • Status is never color alone. On, paused and off, saving, errors, conflicts and the storage meter all have their own words, and the meter reports “72% full”.
  • With reduced motion, rows, groups and notes appear and disappear at once instead of folding.
  • Text uses dir="auto", so a memory in Arabic or Hebrew reads right to left inside an English page.

Construido con

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

Código

10 archivos, añadidos a components/blocks/memory.