Skip to content

Chat

The chat package adds a streaming conversation UI to Lattice. Your application owns the agent, history, authorization, and persistence; the package owns the chat shell, message rendering, and the NDJSON transport between the browser and those endpoints.

Assistant messages are component trees rather than plain strings. Text and tool calls ship with the package, and a stream can insert any other registered Lattice component as a message part.

ChatBox::make('assistant')
->title('Lattice assistant')
->placeholder('Ask about your project');

The preview shows the endpoint-free shell. Add a stream endpoint to enable the composer, and optionally a history endpoint to seed the conversation.

Terminal window
composer require lattice-php/chat

Composer is the only install. The lattice() Vite plugin discovers the package through Composer and registers its React renderer automatically. Pages without a chat box do not load the chat component chunk. For a no-build app, run php artisan lattice:assets after installation.

use Lattice\Chat\Components\ChatBox;
ChatBox::make('support-assistant')
->title('Support assistant')
->placeholder('How can we help?')
->streamEndpoint(route('chat.stream'))
->historyEndpoint(route('chat.history'));

streamEndpoint() is required for sending messages. historyEndpoint() is optional; when present, the component fetches it once after mounting. Both may be relative same-origin URLs or absolute URLs configured through a remote source.

The browser keeps the active conversation in memory. Sending a message adds the user message and an empty assistant message immediately, then folds streamed frames into that assistant message as they arrive.

The endpoints are ordinary application routes. Lattice does not register chat routes or prescribe an AI provider, database model, queue, or conversation identifier.

The history endpoint receives a GET request and returns one object containing messages. Every message needs a stable id, a user or assistant role, and a list of serialized component parts:

{
"messages": [
{
"id": "message-1",
"role": "user",
"parts": [
{
"type": "chat.part.text",
"props": { "text": "Where is my order?" }
}
]
}
]
}

A failed history request leaves the conversation empty. Authentication and authorization remain the route’s responsibility.

The stream endpoint receives a JSON POST body containing only the newly submitted text:

{ "message": "Where is my order?" }

It responds with newline-delimited JSON. Each line is one frame:

Frame Shape Effect
text {"type":"text","value":"Hello"} Appends text to the current assistant text part.
part {"type":"part","part":{...node}} Appends one complete registered Lattice component.
done {"type":"done"} Marks the turn idle.
error {"type":"error","message":"Please try again"} Stops the turn and displays an inline error.

Consecutive text frames merge into one text part. A part frame closes the current text run, so later text starts a new part after that component.

A minimal Laravel controller can stream an application-defined agent directly:

use App\Chat\Assistant;
use Illuminate\Http\Request;
use Lattice\Chat\ChatPart;
use Symfony\Component\HttpFoundation\StreamedResponse;
final readonly class ChatAgentController
{
public function __construct(private Assistant $assistant) {}
public function __invoke(Request $request): StreamedResponse
{
$message = $request->string('message')->trim()->toString();
return response()->stream(function () use ($message): void {
$write = static function (array $frame): void {
echo json_encode($frame, JSON_THROW_ON_ERROR)."\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
};
foreach ($this->assistant->stream($message) as $text) {
$write(['type' => 'text', 'value' => $text]);
}
$write([
'type' => 'part',
'part' => ChatPart::toolCall('lookup-order')->jsonSerialize(),
]);
$write(['type' => 'done']);
}, 200, [
'Content-Type' => 'application/x-ndjson',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
}
}

The no-cache and buffering headers keep reverse proxies from batching the response. A non-success HTTP response, a missing response body, or an interrupted request is surfaced in the chat box as an inline error.

ChatPart::text($text) and ChatPart::toolCall($name, $args) build the two bundled parts. Use them when preparing history or emitting complete part frames:

use Illuminate\Support\Str;
use Lattice\Chat\ChatMessage;
use Lattice\Chat\ChatPart;
use Lattice\Chat\Enums\ChatRole;
$message = new ChatMessage(
id: (string) Str::uuid(),
role: ChatRole::Assistant,
parts: [
ChatPart::text('I found the order.'),
ChatPart::toolCall('lookup-order', ['order' => 'A-1042']),
],
);

A part frame may also contain a regular Lattice component, such as a card, section, data list, or action. It renders through the same registry as the surrounding page, so its node type must be registered in the consuming application.

For a dedicated message component, extend ChatPart and give it a stable wire type with #[AsChatPart]:

use Lattice\Chat\Attributes\AsChatPart;
use Lattice\Chat\ChatPart;
#[AsChatPart('chat.part.order-status')]
final class OrderStatusPart extends ChatPart
{
public string $status = '';
public static function make(string $status): self
{
$part = new self;
$part->status = $status;
return $part;
}
}

Register the matching React renderer exactly like any other custom component. AsChatPart extends Lattice’s component attribute, so discovery, serialization, generated TypeScript props, and registry rendering follow the normal component-package path.

ChatBox supports Lattice’s browser-token flow when the endpoints belong to another service:

ChatBox::make('assistant')
->source('support')
->audience('https://support.example.test')
->scopes(['chat.read', 'chat.write'])
->streamEndpoint('https://support.example.test/api/chat/stream')
->historyEndpoint('https://support.example.test/api/chat/history');

source() and audience() are required together. The browser exchanges the sealed component reference for a short-lived token, sends it as a bearer token to both remote endpoints, and omits cookies. See Remote components for source registration, host allow-lists, and token issuance.

The box is 20rem wide and 28rem tall by default. ->fill() makes it fill its container, with a 28rem minimum height, which suits a page column or panel. ->title() replaces the translated header and ->placeholder() customizes the composer prompt.

The component’s strings ship with inline English defaults. With laravel-i18next enabled, the plugin’s chat namespace loads automatically and serves the bundled en/de translations. Override them like any Laravel package translation; see Internationalization.

The report measures the precompiled chat plugin. React and the framework runtime remain external, so this is the JavaScript the package adds to a Lattice application.

3.6 KB gzipped · 10.1 KB raw JavaScript

DependencyRawGzipShare
@lattice-php/chat7.6 KB3.0 KB82.7%
Bundler runtime2.5 KB0.6 KB17.3%

Emitted JavaScript files

FileRawGzip
plugin.js10.1 KB3.6 KB

Open the full interactive treemap ↗ · Generated by Sonda 0.14.0 on Sat, 22 Aug 2026 17:50:38 GMT.