Skip to content
HyperUX Experimental
Demo

Stream-ready AI chat scaffold with suggested prompts, auto-scroll, thinking messages, message rating, chat export, and persistent voice toggle.

AI Chat

Alpine.js Chat

huxChat is a logic scaffold for ChatGPT/Claude-style UIs. It manages a message thread, loading state, input handling, message actions, suggested prompts, thinking messages, response rating, chat export, and a voice toggle — without coupling you to any specific backend or streaming protocol.

Replace the _simulateResponse() call in sendMessage() and regenerateResponse() with your own fetch/SSE/WebSocket logic to connect to any AI backend.

copyToClipboard() uses the Clipboard API, which requires a secure context (HTTPS or localhost) and, in some browsers, a user gesture. Copy actions will silently fail (with a logged error) in non-secure contexts.

API

huxChat(config)

Returns an Alpine data object with:

Internal helper methods are private implementation details and are not part of the supported API contract.

Options

Quick Start

Minimal

<div x-data="huxChat()">
  <div x-ref="messageList" style="height: 400px; overflow-y: auto;">
    <template x-for="message in messages" x-bind:key="message.id">
      <div x-text="message.content" style="white-space: pre-wrap;"></div>
    </template>
  </div>

  <textarea
    x-model="inputValue"
    x-on:keydown="handleKeydown($event)"
    x-bind:disabled="isLoading"
    placeholder="Message…"
  ></textarea>

  <button type="button" x-on:click="sendMessage()">Send</button>
</div>

With Suggested Prompts

huxChat({
  suggestedPrompts: [
    'What is Alpine.js?',
    'How do I use x-data?',
    'Show me a simple counter example',
  ],
})

Common Usage Patterns

Pre-populate with a Greeting

huxChat({
  messages: [
    {
      id: 'greeting',
      role: 'assistant',
      content: 'Hello! How can I help you today?',
      status: 'complete',
      rating: null,
    },
  ],
})

Connect to a Real AI API

Replace _simulateResponse() in sendMessage() and regenerateResponse() with your own logic. The scaffold adds a sending assistant message before the call so your streaming loop can update message.content in place. Set this.thinkingMessage at key points to surface interim status:

// API Integration Point
async function streamResponse(messages, onChunk, onDone) {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ messages }),
  })

  const reader = response.body.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    onChunk(decoder.decode(value))
  }

  onDone()
}

Thinking Messages

thinkingMessage is a reactive string you set at key points in your integration to surface interim status (“Working on it…”, “Crunching the numbers…”). The demo stub cycles through example messages automatically. Clear it (this.thinkingMessage = '') once the response arrives:

<template x-if="message.status === 'sending' && thinkingMessage !== ''">
  <span x-text="thinkingMessage" aria-live="polite"></span>
</template>

Message Actions on Hover

Wire copyToClipboard(message.id), rateMessage(message.id, 'up'/'down'), and regenerateResponse(message.id) to action buttons inside the message template:

<template x-if="message.role === 'assistant' && message.status === 'complete'">
  <div>
    <button type="button" x-on:click="copyToClipboard(message.id)" aria-label="Copy message">
      Copy
    </button>

    <button
      type="button"
      x-on:click="rateMessage(message.id, 'up')"
      x-bind:aria-pressed="(message.rating === 'up').toString()"
      aria-label="Thumbs up"
    >
      👍
    </button>

    <button
      type="button"
      x-on:click="rateMessage(message.id, 'down')"
      x-bind:aria-pressed="(message.rating === 'down').toString()"
      aria-label="Thumbs down"
    >
      👎
    </button>

    <button
      type="button"
      x-on:click="regenerateResponse(message.id)"
      aria-label="Regenerate response"
    >
      Regenerate
    </button>
  </div>
</template>

Export Chat

exportChat() downloads the full conversation as a plain-text file (chat-export.txt). Only complete messages are included. Bind it to a toolbar button:

<button
  type="button"
  x-on:click="exportChat()"
  x-bind:disabled="messages.filter(m => m.status === 'complete').length === 0"
>
  Export chat
</button>

Voice Toggle

The voice preference is read from and written to localStorage under the key hux-chat:voice-enabled. Bind toggleVoice() to a button and read isVoiceEnabled to drive your TTS logic. The component manages state and persistence only — wire up your own speechSynthesis or voice API call:

<button
  type="button"
  x-on:click="toggleVoice()"
  x-bind:aria-pressed="isVoiceEnabled.toString()"
  x-bind:aria-label="isVoiceEnabled ? 'Disable voice' : 'Enable voice'"
>
  <span x-text="isVoiceEnabled ? 'Voice on' : 'Voice off'">Voice off</span>
</button>

Auto-Expanding Textarea

Use an input handler on the textarea to grow it as the user types:

<textarea
  x-ref="chatInput"
  x-model="inputValue"
  x-on:keydown="handleKeydown($event)"
  x-on:input="$event.target.style.height = 'auto'; $event.target.style.height = $event.target.scrollHeight + 'px'"
  rows="1"
  style="resize: none; overflow: hidden;"
></textarea>

Behavior Contract

Error Handling

Accessibility Notes

Notes