Real-Time Streaming Chat UI with Livewire - Ship AI with Laravel EP7
Chapters10
The episode explains why streaming chat responses are superior to waiting for a single JSON reply, highlighting a real-time chat UI where the agent’s answer streams live instead of showing a static loading state.
Real-time chat UI with Livewire streams AI responses token-by-token using server-sent events, no more loading spinners.
Summary
Harris from Laravel News walks through building a real-time chat UI that streams the agent’s response as tokens are generated. The key idea is to replace the traditional single-shot JSON reply with a streaming flow using server-sent events, so users see the answer building live in their chat window. He contrasts the old approach (3-5 seconds for a full reply) with streaming, where tokens arrive and render as they’re produced. The backend uses a streamable agent and a private resolveAgent method to manage conversation continuity, reusing an existing conversation if one exists. On the front end, Livewire (in class-based mode) handles state, while Alpine.js handles the streaming connection to the /chat/stream endpoint. The UI showcases a polished chat interface with a dedicated support chat Livewire component, a simple messages array, an input field, and a streaming flag to indicate live generation. Harris also notes practical tips and gotchas, like the need to attach the conversation ID from the streaming result and the option to broadcast updates via WebSockets for multi-client dashboards. Finally, he teases future use cases like querying live data from a knowledge base or carrier statuses in real time.
Key Takeaways
- Streaming sends tokens as they are generated and renders them in the browser, reducing perceived latency dramatically.
- A single-line change in the SDK enables the streaming capability by handling text delta events and forwarding them to the client via server-sent events.
- The chat controller now includes a stream method that validates input, creates or continues a conversation, and yields text delta events to the frontend.
- The front end combines a Livewire class-based component with Alpine.js to establish a streaming connection to /chat/stream and render updates in real time.
- The conversational ID is attached to the inner response object and requires a then callback after streaming completes to preserve continuity for follow-up messages.
Who Is This For?
Laravel developers building customer-facing chat widgets who want real-time, spinner-free interactions and scalable, streaming UI patterns. Especially useful for those integrating Livewire, Alpine.js, Breeze, and the Laravel AI SDK.
Notable Quotes
"We forward them to the browser using server-sent events. The user sees the response built in real-time."
—Explains the core streaming mechanism delivering tokens to the browser.
" streaming flips that. The provider sends tokens as they're generated."
—Highlights the switch from single-shot responses to token-by-token streaming.
"The SDK gives us a streaming wrapper, but the conversational ID gets attached to the inner response object that SDK builds during iteration."
—Notes a subtle gotcha important for follow-up messages.
"We’re using Livewire for the chat component and Alpine JS for the streaming connection."
—Describes the front-end stack that ties the UI to streaming data.
"The response comes in streaming now. It’s pretty cool."
—Demonstrates the real-time UX in action.
Questions This Video Answers
- How does Laravel stream real-time chat responses with Livewire and Alpine.js?
- What are text delta events in the Laravel AI SDK and how are they used for streaming?
- How can I implement a chat UI that continues conversations across sessions in Laravel?
- What’s the difference between server-sent events and WebSockets for live chat in Laravel?
- How do you wire a streaming endpoint like /chat/stream in a Laravel app with Breeze and Livewire?
Laravel NewsLivewireAlpine.jsLaravel AI SDKServer-Sent EventsStreaming UIChat UIWebSocketsBreezeLaravel route design
Full Transcript
We've been testing our support agent through routes and JSON responses, but customers don't talk to JSON endpoints, right? They use chat widgets, and nobody wants to stare at a loading spinner for 5 seconds while the AI thinks. I'm Harris from Laravel News, and today we're building a real-time chat UI where the agent's response streams in live word by word. Let's get right to it. So, what we call prompt, [music] the entire response generates on the provider side and comes back one shot. works. I think users sits there waiting. Streaming flips that. The provider sends tokens as they're generated.
We forward them to the browser using server-sent events. The user sees the response built [music] in real-time. So, one line change in the SDK. The comparison was very simple. We have agent prompt, what we have so far, and then we call response text. This takes around 3 to 5 seconds versus agent streaming, which is tokens flow to the browser as it's generated, so it's almost immediate. Let's build the full chat interface now. In episode four, we built a chat controller with a send method that returns JSON. Now, we need a stream method for real-time responses.
Both methods resolve the agent the same way, so let's extract that into the private method. Let's go with our chat controller. Chat controller, let's go here. And as you can see now, two new methods here. The first one is going to be stream. And in this one, as you can see, we accept two parameters, the message and the conversation ID. Once we validate those, we use streamable property, and we resolve that agent out of this resolve agent function down here. So, what this does is it gives us a new support agent, the support agent we built so far.
If And again, if we have conversation ID, it just continues from where we left off. Otherwise, it creates a new instance. Going going back here, you see that we call response stream. And in here, we use the streamable property we have. And this streamable property is actually a series of events. So, we check, is this event instance of text delta? And what this text delta is, it comes from the Laravel AI SDK. It's an event. So, if it is, then we yield that back into the browser. And we also yield some more information, just like the conversation ID or the content type down here.
Those headers. We need the text event stream headers. And we're ready in this case. A few things here. The resolve agent is a private method. Don't forget that. Both send and stream use. So, it creates agents with the authenticated user, and either starts or continues the conversation, just just like we mentioned. So, the stream method iterates stream events manually, instead of returning the SDK response directly. We filter for the text delta. Let's scroll down here. Let's create a new route. It's going to be a new post route in this case. And we're going to do chat stream.
So, chat {slash} stream. And now, what we want in this case is to call chat controller, the class, and then call the stream method, of course. Let's also pass middleware, because we want this to be only for authenticated users. Perfect. So, chat stream is in place now. Now, the front end. Livewire for the chat component, Alpine JS for the streaming connection. So, Alpine ships with Livewire. No extra dependencies in this case. Let's go ahead and create one of Livewire components. PHP Artisan make Livewire support chat. We're going to call that one support chat. And let's use the class flag here.
What this class flag does, it gives us a transitional class-based component with a separate PHP class and blade view. So, Livewire defaults to a single file component now, but class-based keeps things organized for something this size. Let's open up support chat. And here you go. The Livewire component manages conversation state. So, let's go ahead and add some properties here. So, the first one is going to be a public array of messages. This is going to hold all our messages. That's pretty straightforward. The next one is going to be a string about the input of the user.
So, public string input and then this will be an empty string in this case. Next one, the conversation ID. So, public Again, this might not be available, so we're going to make this nullable as well. So, conversation ID equals null. And last but not least, we're going to have a property, a boolean, to see if it's actually streaming or not. So, public bool is streaming and this one will default that to false in this case. Perfect. So, this is intentionally minimal. Messages array for display, conversation ID for continuity, a streaming flag. The heavy lifting happens in blade template with Alpine.js.
Let's open up support chat.blade file now. Now, the template. I've put together a polished chat interface with all the styling done. I won't walk through CSS though in this video. This is not what this series is about. The full template is in GitHub repo linked in the description. Copy and paste it. In our app.css file, you can see everything we have here from the theme we use, the keyframes, our background and everything. So, you can copy and paste that from the repository we have in GitHub. Now, in supportchat.blade.php file, this is where our main template lives.
We have the support chat up here, which is from Alpine. If we scroll down, you'll see we have the function down here. So, this is how we use Alpine inside Livewire. We also have send message using asynchronous and all. And then down here, the main thing you need to know is we fetch chat/stream. We use the post method. It's exactly the the endpoint we used in our routes file. So, we use application/json as content type, the X CSRF token, and the body with message and conversation ID if exists. That's mainly all you need to know in this case.
The Alpine component is where it comes together all of this. So, when the user sends a message, we open a streaming connection to our API with the fetch I'll just show you and readable stream. As each event chunk arrives, we pass out the text. We append it to the agent's message bubble in real time. Let's now create a page for the chat widget. We have Breeze installed, so we'll extend its layout. Let's go to our routes file, and now we create a get request. So, route get chat. This is going to be our get request.
So, now let's do function here. And what do we need in here? We just need to return a view we're going to create. And this view will be called chat as well. Of course, don't forget to add the middleware here because we want this to be only for the authorized users, of course. So, off. And let's give this a name. We're going to name this chat. Now, let's create the view. Inside our resources, views, open this up, and let's create up here a new file called chat.blade.php. And let's paste our template. This is very clean.
It uses Breeze layout, which gives us the navbar and consistent styling. We drop in the Livewire component. So, as you can see, we have support chat Livewire component down here. And let's see this in the browser. Let's go back to our browser now, and let's call /chat. We need to log in first, and this is what we have. Let me zoom out a bit. This is what we see. As you can see, we have three main buttons here as a quick start. It says, "How can we assist you?" And you can ask anything about your orders, shipping, returns, or policies.
That's pretty beautiful, I would say. So, this is our chat widget. So, what should we type here? I suggest we ask about our order. Let's say that we arrived like damaged or something. And what should we do about it? My order 1042 arrived damaged. What should I do? Also, what's your refund timeline? Let's see this going. You can see the response coming in as a streaming now. It's pretty cool. I'm sorry your order arrived damaged. I found the order and it showed as shipped. For damaged items, please take clear photos, etc., etc. And also talks about the refund timeline.
Refunds are typically processed within 5 to 7 business days after the returned items received. Pretty neat, right? So, the agent hit order lookup for the order 1042, searched the knowledge base for the damaged item policy, and streamed back the detailed response with specific instructions, all in real time without any spinner in this case. And because we passed the conversation ID, follow-ups work. The agent remembers everything. Quick gotcha here. The SDK gives us a streaming wrapper, but the conversational ID gets attached to the inner response object that SDK builds during iteration. So, if you reach for the streamable conversational ID directly, you'll always get null, and your follow-up messages will start a brand new conversation.
The fix is to register a then callback that fires when streaming completes with a fully built response. And that's where the conversational ID lives. We capture this into variable by reference and emit it after the for each. Let's continue now. One more thing. If you need to push updates to multiple clients like a support dashboard where agents watch incoming chats, the SDK supports broadcasting via WebSockets. So, keep that in mind. Livewire component, Alpine JS for streaming, server-side event endpoint forwarding tokens as they generate. The experience went from wait and hope to watching the response build live.
Big difference for the users. Next up, web search. The agent will look up live data within knowledge base for source. Carrier statuses, product availability, things that change by the minute. See you in the next one.
More from Laravel News
Get daily recaps from
Laravel News
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.









