Chat Interactions
Manages chat sessions, routes responses through pluggable handlers, and tracks interaction history.
If you want the easiest playground-style UI for a new host, start here after you have one provider connection and one deployment configured. Unlike AI Chat, Chat Interactions do not require an AI Profile to get started.
The prompt security layer documented in Prompt Security is designed for AI Profile-based chat. Chat Interactions remain intentionally operator-controlled and do not enable that profile security layer by default.
Quick Start
builder.Services.AddCrestAppsCore(crestApps => crestApps
.AddAISuite(ai => ai
.AddOpenAI()
.AddChatInteractions(chatInteractions => chatInteractions
.AddEntityCoreStores()
)
)
.AddEntityCoreSqliteDataStore("Data Source=app.db")
);
By default, connections are discovered from CrestApps:AI:Connections and deployments are discovered from CrestApps:AI:Deployments. Connection-based deployments can reference a shared ConnectionName, while contained-connection deployments can embed provider-specific settings directly in the deployment entry.
When you are ready to turn an ad hoc interaction into a reusable runtime contract, move that setup into an AI Profile.
Registering Chat Interaction Stores
The chat interactions feature requires stores for ChatInteraction and IChatInteractionPromptStore. Register stores directly on the chat interactions builder:
Entity Framework Core (via builder):
.AddChatInteractions(chatInteractions => chatInteractions
.AddEntityCoreStores()
)
YesSql (via builder):
.AddChatInteractions(chatInteractions => chatInteractions
.AddYesSqlStores()
)
Both register an ICatalog<ChatInteraction> and IChatInteractionPromptStore implementation. See Data Storage for the full per-feature store reference.
Problem & Solution
A chat experience involves more than sending messages to an LLM:
- Sessions must be created, tracked, and eventually closed
- History needs to be persisted so users can resume conversations
- Routing determines whether a message goes to an AI orchestrator, a live agent, or an external webhook
- Interactions are discrete conversation units within a session that can be transferred between handlers
The chat system provides all of this with a pluggable handler architecture.
In the MVC sample, Chat Interactions now reserve automatic spoken playback for active conversation mode only. Typed prompts and microphone dictation still produce normal streamed text responses, but they no longer auto-read the assistant reply unless the user explicitly started the live two-way conversation flow.
Both the MVC and Blazor sample hosts now render [doc:n] citations as superscript markers in assistant responses and show the resolved document references as clickable links directly below the cited message. When a citation points to an attached AI document, the reference link now downloads that file from the server after the host registers both AddReferenceDownloads() on the document-processing builder and AddDownloadAIDocumentEndpoint() on the endpoint route builder.
If a host needs to merge preemptive-RAG references with tool-generated references during streaming, AddCoreAIChatInteractions() now registers CitationReferenceCollector plus CompositeAIReferenceLinkResolver so the host can reuse the shared citation-merging logic instead of duplicating it per UI project.
Services Registered by AddCoreAIChatInteractions()
| Service | Implementation | Lifetime | Purpose |
|---|---|---|---|
ChatInteractionCompletionContextBuilderHandler | — | Scoped | Enriches completion context with chat history |
ChatInteractionEntryHandler | — | Scoped | Catalog lifecycle handler for ChatInteraction |
CitationReferenceCollector | CitationReferenceCollector | Scoped | Merges preemptive and tool-generated citations, resolves reference links, and tracks referenced articles |
DataExtractionService | DataExtractionService | Scoped | Extracts configured fields from completed chat turns |
PostSessionProcessingService | PostSessionProcessingService | Scoped | Runs AI-powered post-session tasks and evaluations |
AIChatSessionCloseCycleService | AIChatSessionCloseCycleService | Singleton | Runs one shared inactivity-close and post-close retry cycle so any host can reuse the framework logic without owning the implementation |
AIChatSessionCloseRunner | AIChatSessionCloseRunner | Singleton | Starts the shared session-close cycle on startup and then every 5 minutes through reusable StartAsync / StopAsync methods |
AIChatSessionCloseBackgroundService | AIChatSessionCloseBackgroundService | Singleton (IHostedService) | Thin default hosted wrapper that delegates to AIChatSessionCloseRunner |
DataExtractionChatSessionHandler | — | Scoped | Runs shared extraction and closes sessions on natural farewells |
PostSessionProcessingChatSessionHandler | — | Scoped | Triggers the shared post-close processor when a session closes |
The chat system also registers embedded templates from the CrestApps.Core.AI.Chat assembly for system prompts.
Core Concepts
Chat Session (AIChatSession)
A session represents a conversation between a user and the AI system. It has:
- Status — Active, Closed, Expired
- ResponseHandlerName — Which handler processes messages (e.g.,
"ai","genesys") - Attached documents — Files uploaded for RAG processing
- Metadata — Custom key-value data
Chat Interaction (ChatInteraction)
An interaction is a unit of conversation within a session. When a response handler transfers the conversation (e.g., from AI to a live agent), a new interaction is created while the session continues.
Response Handler
A pluggable component that decides how to process a chat message. The default handler (AIChatResponseHandler) routes through the AI orchestrator. Custom handlers can route to external systems like Genesys, Twilio Flex, or custom webhooks.
Key Interfaces
IChatResponseHandler
Implement this to create a custom response routing strategy.
public interface IChatResponseHandler
{
string Name { get; }
Task<ChatResponseHandlerResult> HandleAsync(
ChatResponseHandlerContext context,
CancellationToken cancellationToken = default);
}
The Name property identifies the handler. It is stored on the session or interaction so the system knows which handler to use for subsequent messages.
See Response Handlers for detailed implementation guidance.
IChatResponseHandlerResolver
Resolves a handler by name at runtime.
public interface IChatResponseHandlerResolver
{
IChatResponseHandler Resolve(string name);
}
ICatalogEntryHandler<ChatInteraction>
React to chat interaction lifecycle events (creating, created, updating, etc.).
public sealed class MyChatInteractionHandler : CatalogEntryHandlerBase<ChatInteraction>
{
public override Task CreatedAsync(CreatedContext<ChatInteraction> context)
{
// React to new interaction
}
}
Chat document authorization
The shared document endpoints use standard ASP.NET Core resource authorization through IAuthorizationService.
Hosts register IAuthorizationHandler implementations for:
AIChatDocumentOperations.ManageDocumentsonChatInteractionAIChatDocumentOperations.ManageDocumentsonAIChatSessionDocumentAuthorizationContext
AIChatSessionDocumentAuthorizationContext carries both the AIProfile and AIChatSession, so hosts can apply different rules for admin-managed interaction documents versus end-user session uploads without introducing a separate chat-specific authorization abstraction.
IAIChatDocumentEventHandler
Implement this hook when uploaded or removed chat documents need additional side effects such as indexing chunks, persisting original files, or cleaning up external stores.
public interface IAIChatDocumentEventHandler
{
Task UploadedAsync(AIChatDocumentUploadContext context, CancellationToken cancellationToken = default);
Task RemovedAsync(AIChatDocumentRemoveContext context, CancellationToken cancellationToken = default);
}
Session Lifecycle
A chat session moves through a well-defined lifecycle:
NewAsync() SaveAsync() (inactivity / explicit close)
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Active │──────▶│ Active │───────────────▶│ Closed │
└────────┘ └────────┘ └────────┘
Created LastActivityUtc updated ClosedAtUtc set
| Stage | What Happens |
|---|---|
| Creation | IAIChatSessionManager.NewAsync() allocates a new AIChatSession, assigns a SessionId, sets Status = Active, records CreatedUtc, and associates it with the profile and user. |
| Active Use | Every user message updates LastActivityUtc. Prompts are appended via IAIChatSessionPromptStore. Documents may be attached to session.Documents. |
| Interaction Transfer | If a response handler transfers the conversation (e.g., AI → live agent), a new ChatInteraction is created while the session continues. The session's ResponseHandlerName updates to the new handler. |
| Closure | The session status changes to Closed and ClosedAtUtc is recorded. The shared post-close processor updates extraction state, post-session task results, resolution analysis, and conversion-goal evaluation so hosts reuse the same runtime behavior. |
| Deletion | DeleteAsync() removes the session and its associated prompts. DeleteAllAsync() removes all sessions for a given profile and user. |
AddCoreAIChatSessionProcessing() now registers the shared AIChatSessionCloseCycleService, reusable AIChatSessionCloseRunner, and the default hosted wrapper AIChatSessionCloseBackgroundService, so any host that enables the standard AI chat session pipeline and registers AI chat session stores gets the same inactivity-closing and post-close retry behavior without adding a host-specific worker. Hosts that use a different scheduling system can call the runner or one-cycle service directly instead of reimplementing the framework logic. The default runner starts immediately and then runs every 5 minutes.
Key Properties of AIChatSession
| Property | Type | Description |
|---|---|---|
SessionId | string | Unique session identifier |
ProfileId | string | Associated AI profile |
Title | string | Human-readable title (often AI-generated after the first exchange) |
UserId | string | Authenticated user who owns the session |
ClientId | string | Anonymous client identifier (used when UserId is null) |
Status | ChatSessionStatus | Active, Closed, etc. |
ResponseHandlerName | string | Which IChatResponseHandler processes messages |
Documents | List<ChatDocumentInfo> | Uploaded files for RAG processing |
CreatedUtc | DateTime | When the session started |
LastActivityUtc | DateTime | Last message timestamp |
ClosedAtUtc | DateTime? | When the session was closed |
ExtractedData | Dictionary<string, ExtractedFieldState> | Extracted conversation fields |
PostSessionProcessingStatus | PostSessionProcessingStatus | Status of post-session tasks |
Each PostSessionResults entry now keeps AttemptHistory for failed or incomplete retries, and ProcessedAtUtc is only populated once the task reaches a terminal success or final failure state. Pending retries keep their last attempt details in history instead of surfacing a default timestamp. Post-session tool resolution also honors task-scoped PostSessionTask.ToolNames in addition to profile-level post-session tool configuration, and post-close retries default to 5 attempts before the task is treated as terminally failed.
Hosts can override that retry cap through the shared AIChatSessionProcessingOptions.MaxPostCloseAttempts site setting. The MVC admin settings page surfaces the same value as Max post-close attempts, and the shared processor reads it through IOptionsMonitor<> so both the default hosted runner and custom schedulers honor the same limit.
When the model returns valid structured JSON with an empty tasks array, the framework now records that as an explicit post-session failure instead of a misleading JSON-parse error. If the tool-enabled response returns invalid task entries such as empty names or values, the framework now runs a structured recovery pass and, when no tool calls actually happened, retries the same work through the structured no-tools path before treating the attempt as failed. The shared post-session prompts also require one result per configured task, even when the task decides not to call a tool.
Extracted Data Reporting Snapshots
The framework now exposes IAIChatSessionExtractedDataStore for querying extracted-data reporting snapshots and ships a default IAIChatSessionExtractedDataRecorder that writes to that store whenever extraction produces new values or a session naturally closes.
public interface IAIChatSessionExtractedDataStore
{
Task SaveAsync(
AIChatSessionExtractedDataRecord record,
CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(
string sessionId,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<AIChatSessionExtractedDataRecord>> GetAsync(
string profileId,
DateTime? startDateUtc,
DateTime? endDateUtc,
CancellationToken cancellationToken = default);
}
When you register chat session stores with YesSql or EntityCore, the extracted-data store and default recorder are registered automatically as part of the chat feature extensions. Hosts can still add additional IAIChatSessionExtractedDataRecorder implementations for custom side effects, but they no longer need to implement snapshot persistence just to power extracted-data reports.
Session Management
The framework defines IAIChatSessionManager for session CRUD. You must provide an implementation since session storage is application-specific. The MVC example uses a YesSql-backed implementation:
builder.Services.AddScoped<IAIChatSessionManager, YesSqlAIChatSessionManager>();
builder.Services.AddScoped<IAIChatSessionPromptStore, YesSqlAIChatSessionPromptStore>();
The MVC sample also registers an AIChatSessionCloseBackgroundService that runs every 5 minutes, closes inactive sessions, retries pending post-close processing, and keeps analytics / extracted-data reporting records aligned with the final session state.
Shared document endpoints
The framework now ships reusable minimal API extensions for chat document uploads and removals:
app.AddUploadChatInteractionDocumentEndpoint()
.AddRemoveChatInteractionDocumentEndpoint()
.AddUploadChatSessionDocumentEndpoint()
.AddRemoveChatSessionDocumentEndpoint();
The built-in document endpoints commit staged store writes before returning so uploaded files, removed files, and updated document metadata persist across reloads. When you build your own minimal APIs on top of the same store abstractions, apply StoreCommitterEndpointFilter or call IStoreCommitter.CommitAsync() explicitly before returning.
These endpoints:
- process files through
IAIDocumentProcessingService - persist
AIDocumentandAIDocumentChunkrecords through the configured stores - update
ChatInteraction.DocumentsorAIChatSession.Documents - authorize upload and remove operations through
IAuthorizationService - invoke
IAIChatDocumentEventHandlerso the host can index chunks or save original files
Session uploads are also gated by AIProfileSessionDocumentsMetadata.AllowSessionDocuments, which keeps profile-level session upload behavior explicit.
IAIChatSessionManager Interface
public interface IAIChatSessionManager
{
Task<AIChatSession> FindByIdAsync(string id);
Task<AIChatSession> FindAsync(string id);
Task<AIChatSessionResult> PageAsync(int page, int pageSize, AIChatSessionQueryContext context = null);
Task<AIChatSession> NewAsync(AIProfile profile, NewAIChatSessionContext context);
Task SaveAsync(AIChatSession chatSession);
Task<bool> DeleteAsync(string sessionId);
Task<int> DeleteAllAsync(string profileId);
}
| Method | Purpose |
|---|---|
FindByIdAsync | Retrieves a session by ID (no ownership check) |
FindAsync | Retrieves a session by ID with ownership verification |
PageAsync | Paginated listing with optional query filters |
NewAsync | Creates a new session for a profile |
SaveAsync | Persists changes to a session |
DeleteAsync | Deletes a single session by ID |
DeleteAllAsync | Deletes all sessions for a profile and current user |
Implementing IAIChatSessionManager (YesSql Example)
Below is a simplified YesSql-based implementation following the pattern used in the MVC sample:
public sealed class YesSqlAIChatSessionManager : IAIChatSessionManager
{
private readonly ISession _session;
private readonly IClock _clock;
private readonly IHttpContextAccessor _httpContextAccessor;
public YesSqlAIChatSessionManager(
ISession session,
IClock clock,
IHttpContextAccessor httpContextAccessor)
{
_session = session;
_clock = clock;
_httpContextAccessor = httpContextAccessor;
}
public async Task<AIChatSession> FindByIdAsync(string id)
{
return await _session
.Query<AIChatSession, AIChatSessionIndex>(x => x.SessionId == id)
.FirstOrDefaultAsync();
}
public async Task<AIChatSession> FindAsync(string id)
{
var chatSession = await FindByIdAsync(id);
if (chatSession == null)
{
return null;
}
// Verify ownership
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
if (chatSession.UserId != userId)
{
return null;
}
return chatSession;
}
public async Task<AIChatSession> NewAsync(AIProfile profile, NewAIChatSessionContext context)
{
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
var chatSession = new AIChatSession
{
SessionId = IdGenerator.GenerateId(),
ProfileId = profile.Id,
UserId = userId,
ClientId = context?.ClientId,
Status = ChatSessionStatus.Active,
ResponseHandlerName = profile.ResponseHandlerName ?? "ai",
CreatedUtc = _clock.UtcNow,
LastActivityUtc = _clock.UtcNow,
};
await _session.SaveAsync(chatSession);
await _session.SaveChangesAsync();
return chatSession;
}
public async Task SaveAsync(AIChatSession chatSession)
{
await _session.SaveAsync(chatSession);
await _session.SaveChangesAsync();
}
public async Task<bool> DeleteAsync(string sessionId)
{
var chatSession = await FindByIdAsync(sessionId);
if (chatSession == null)
{
return false;
}
_session.Delete(chatSession);
await _session.SaveChangesAsync();
return true;
}
public async Task<int> DeleteAllAsync(string profileId)
{
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
var sessions = await _session
.Query<AIChatSession, AIChatSessionIndex>(x =>
x.ProfileId == profileId && x.UserId == userId)
.ListAsync();
var count = 0;
foreach (var session in sessions)
{
_session.Delete(session);
count++;
}
if (count > 0)
{
await _session.SaveChangesAsync();
}
return count;
}
public async Task<AIChatSessionResult> PageAsync(
int page, int pageSize, AIChatSessionQueryContext context = null)
{
var query = _session.Query<AIChatSession, AIChatSessionIndex>();
if (!string.IsNullOrEmpty(context?.ProfileId))
{
query = query.Where(x => x.ProfileId == context.ProfileId);
}
var count = await query.CountAsync();
var sessions = await query
.OrderByDescending(x => x.LastActivityUtc)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ListAsync();
return new AIChatSessionResult
{
Count = count,
Sessions = sessions.ToArray(),
};
}
}
Use IdGenerator.GenerateId() (which produces 26-character IDs) for all session and prompt identifiers. Never use Guid.NewGuid().
Implementing IAIChatSessionPromptStore
The prompt store persists the individual messages (user prompts and assistant responses) within a session. It extends ICatalog<AIChatSessionPrompt>.
public interface IAIChatSessionPromptStore : ICatalog<AIChatSessionPrompt>
{
Task<IReadOnlyList<AIChatSessionPrompt>> GetPromptsAsync(string sessionId);
Task<int> DeleteAllPromptsAsync(string sessionId);
Task<int> CountAsync(string sessionId);
}
A YesSql implementation follows the same pattern:
public sealed class YesSqlAIChatSessionPromptStore : IAIChatSessionPromptStore
{
private readonly ISession _session;
public YesSqlAIChatSessionPromptStore(ISession session)
{
_session = session;
}
public async Task<IReadOnlyList<AIChatSessionPrompt>> GetPromptsAsync(string sessionId)
{
var prompts = await _session
.Query<AIChatSessionPrompt, AIChatSessionPromptIndex>(
x => x.SessionId == sessionId)
.OrderBy(x => x.CreatedUtc)
.ListAsync();
return prompts.ToArray();
}
public async Task<int> DeleteAllPromptsAsync(string sessionId)
{
var prompts = await _session
.Query<AIChatSessionPrompt, AIChatSessionPromptIndex>(
x => x.SessionId == sessionId)
.ListAsync();
var count = 0;
foreach (var prompt in prompts)
{
_session.Delete(prompt);
count++;
}
if (count > 0)
{
await _session.SaveChangesAsync();
}
return count;
}
public async Task<int> CountAsync(string sessionId)
{
return await _session
.Query<AIChatSessionPrompt, AIChatSessionPromptIndex>(
x => x.SessionId == sessionId)
.CountAsync();
}
// ICatalog<T> base methods (FindByIdAsync, CreateAsync, etc.)
// follow the same YesSql query pattern.
}
Implementing IChatInteractionPromptStore
The interaction prompt store is similar to the session prompt store but scoped to a ChatInteraction rather than a session. It extends ICatalog<ChatInteractionPrompt>.
public interface IChatInteractionPromptStore : ICatalog<ChatInteractionPrompt>
{
Task<IReadOnlyCollection<ChatInteractionPrompt>> GetPromptsAsync(string chatInteractionId);
Task<int> DeleteAllPromptsAsync(string chatInteractionId);
}
The implementation follows the same YesSql patterns shown above, querying against ChatInteractionPromptIndex with a ChatInteractionId predicate.
Chat Flow Example
Here is the end-to-end flow when a user sends a message through the AIChatHub:
1. Client sends message via SignalR
│
▼
2. AIChatHub.SendMessage()
│
▼
3. Session Resolution (GetOrCreateSessionAsync)
├── If sessionId provided → FindAsync(sessionId)
└── If no sessionId → NewAsync(profile, context)
│
▼
4. Prompt Saved (IAIChatSessionPromptStore.CreateAsync)
└── User message stored as AIChatSessionPrompt
│
▼
5. Response Handler Resolved (IChatResponseHandlerResolver)
└── Looks up handler by session.ResponseHandlerName
│
▼
6. Handler Executes (IChatResponseHandler.HandleAsync)
├── Default "ai" handler:
│ ├── Builds OrchestrationContext
│ ├── Injects conversation history
│ ├── Runs orchestrator (tool calls, RAG, etc.)
│ └── Returns streaming response
└── Custom handler (e.g., "live-agent"):
└── Routes to external system
│
▼
7. Response Streamed to Client
└── Each ChatResponseUpdate is sent via SignalR
│
▼
8. Completion Finalized
├── Assistant response saved as AIChatSessionPrompt
├── Session title generated (if first exchange)
├── LastActivityUtc updated
└── Citations/references collected
Code Walkthrough
// Step 1-3: The hub resolves the session
var session = !string.IsNullOrEmpty(sessionId)
? await sessionManager.FindAsync(sessionId)
: await sessionManager.NewAsync(profile, new NewAIChatSessionContext { ClientId = clientId });
// Step 4: Save the user prompt
var prompt = new AIChatSessionPrompt
{
ItemId = IdGenerator.GenerateId(),
SessionId = session.SessionId,
Role = ChatRole.User,
Content = userMessage,
CreatedUtc = clock.UtcNow,
};
await promptStore.CreateAsync(prompt);
// Step 5: Resolve the response handler
var handler = handlerResolver.Resolve(session.ResponseHandlerName);
// Step 6: Execute the handler
var handlerContext = new ChatResponseHandlerContext
{
Session = session,
UserMessage = userMessage,
// ... additional context
};
var result = await handler.HandleAsync(handlerContext, cancellationToken);
// Step 7: Stream the response
if (!result.IsDeferred)
{
await foreach (var update in result.ResponseStream)
{
await Clients.Caller.ReceiveMessage(update);
}
}
// Step 8: Save assistant response, update session
session.LastActivityUtc = clock.UtcNow;
await sessionManager.SaveAsync(session);
ChatResponseHandlerResult
The result from a handler is either streaming or deferred:
public sealed class ChatResponseHandlerResult
{
public bool IsDeferred { get; init; }
public IAsyncEnumerable<ChatResponseUpdate> ResponseStream { get; init; }
// Factory methods:
public static ChatResponseHandlerResult Deferred();
public static ChatResponseHandlerResult Streaming(IAsyncEnumerable<ChatResponseUpdate> stream);
}
- Streaming — The hub immediately iterates
ResponseStreamand pushes updates to the client via SignalR. - Deferred — The hub saves the user prompt and completes the request. The response arrives later (e.g., via a webhook callback). This is common for live-agent handoff scenarios.
Error Handling
Session Not Found
When FindAsync() returns null, the hub sends a localized error to the client and stops processing. No exception is thrown — this is a normal flow when sessions expire or are deleted.
Completion Failures
If the AI provider throws during completion (e.g., rate limit, timeout, network error), the orchestrator catches the exception and the hub sends an error notification to the client:
try
{
await foreach (var update in result.ResponseStream)
{
await Clients.Caller.ReceiveMessage(update);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error streaming response for session {SessionId}", session.SessionId);
await Clients.Caller.ReceiveError("An error occurred while processing your message.");
}
The user's prompt is saved before the completion call. If completion fails, the prompt remains in history. This is intentional — it preserves the conversation state so the user can retry.
Handler Not Found
If IChatResponseHandlerResolver.Resolve() cannot find a handler matching session.ResponseHandlerName, it falls back to the default AI handler. If no handlers are registered at all, an error is returned to the client.
Profile Not Found
When the requested AI profile does not exist or the user lacks permission, the hub returns a "profile not found" error without creating a session.
Example: Transferring a Conversation
A response handler can transfer a conversation to a different handler mid-session:
public sealed class EscalationHandler : IChatResponseHandler
{
public string Name => "escalation";
public async Task<ChatResponseHandlerResult> HandleAsync(
ChatResponseHandlerContext context,
CancellationToken cancellationToken)
{
// Transfer to live agent system
context.Interaction.ResponseHandlerName = "live-agent";
return ChatResponseHandlerResult.Transferred();
}
}
Rich Content Rendering
The bundled chat UI components (ai-chat.js and chat-interaction.js) support rendering rich content beyond plain text.
Image Generation
When an image-capable deployment is configured (e.g., a DALL-E model), the GenerateImageTool produces image URLs that the chat UI renders inline. No additional client-side libraries are required — generated images are displayed automatically using standard <img> tags.
Interactive Charts
The GenerateChartTool returns Chart.js configuration objects that the chat UI can render as interactive charts. To enable chart rendering, include the Chart.js library on your page:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
When Chart.js is loaded, chart responses are rendered as interactive <canvas> elements. When Chart.js is not loaded, the raw JSON is displayed instead and a warning is logged to the browser console so you know chart rendering is available.
Both tools are registered automatically by the orchestration pipeline and listed under Built-in System Tools.
Client-Side Assets
The JavaScript and CSS files that power the chat UIs are published as an npm package:
npm install @crestapps/ai-chat-ui
After installing, copy the files from node_modules/@crestapps/ai-chat-ui/dist/ into your web application's static assets folder.
Via CDN
All assets are also available through jsDelivr — no install required:
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@crestapps/ai-chat-ui/dist/chat-widget.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@crestapps/ai-chat-ui/dist/document-drop-zone.min.css" />
<!-- JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/@crestapps/ai-chat-ui/dist/ai-chat.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@crestapps/ai-chat-ui/dist/document-drop-zone.min.js"></script>
Pin a specific version in production by appending @<version> after the package name, e.g.
https://cdn.jsdelivr.net/npm/@crestapps/ai-chat-ui@1.0.0/dist/ai-chat.min.js
Package Contents
| File | Description |
|---|---|
ai-chat.js / .min.js | AI Chat widget — sessions, uploads, streaming, full chat UI |
chat-interaction.js / .min.js | Chat Interaction widget — lighter standalone chat experience |
document-drop-zone.js / .min.js | Drag-and-drop file upload component |
technical-name-generator.js / .min.js | Auto-generates URL-safe technical names from display names |
chat-widget.css / .min.css | Styles for the floating AI Chat widget |
document-drop-zone.css / .min.css | Styles for the document drop-zone component |
Each JavaScript and CSS file also ships with a companion .map source map file (e.g. ai-chat.js.map, chat-widget.css.map). Source maps are generated for both the dev and minified builds and are included in the dist/ folder and npm package exports.
Required Client-Side Dependencies
Both widgets require the following libraries to be loaded on the page before the widget script:
| Library | Purpose | CDN Example |
|---|---|---|
| Vue 3 | Reactive UI | https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js |
| marked | Markdown rendering | https://cdn.jsdelivr.net/npm/marked/marked.min.js |
| DOMPurify | HTML sanitization | https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js |
| SignalR | Real-time messaging | https://cdn.jsdelivr.net/npm/@microsoft/signalr/dist/browser/signalr.min.js |
Optional Dependencies
| Library | Purpose | CDN Example |
|---|---|---|
| Chart.js | Interactive chart rendering | https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js |
| highlight.js | Code syntax highlighting | https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js |
| Font Awesome 7 | Message action icons and brand icons such as Claude; works with either the CSS/webfont include or the SVG+JS bundle | https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7/css/all.min.css |
Widget layout behavior
The AI Chat widget now supports draggable and resizable floating shells through the shared widget config. These behaviors are enabled by default for widgets, persist the user-selected position and size in localStorage, and can be turned off per host if needed.
window.openAIChatManager.initialize({
// ... other config ...
widget: {
chatWidgetContainer: '#widget-panel',
chatWidgetStateName: 'support-chat',
toggleButtonSelector: '#widget-toggle',
resetSizeButtonSelector: '#widget-reset-size',
enableDragging: true,
enableResizing: true,
persistLayout: true
}
});
When enableDragging is on, both the widget window and its floating toggle button can be repositioned. When enableResizing is on, the widget can be resized from the browser resize handle and the optional reset-size button can restore the default dimensions. Set either flag to false to opt out for a specific host.
Text-to-Speech Play Icon
When a text-to-speech (TTS) deployment is configured, each completed assistant message shows a play icon that reads the message aloud via server-side speech synthesis. Clicking the icon calls SynthesizeSpeech on the SignalR hub, which streams audio chunks back to the client and plays them as MP3. While audio is active, the icon switches to pause, the action buttons stay anchored at the bottom-right of the message just above the divider, and starting playback on a different message automatically stops the current player.
To enable this feature, pass textToSpeechEnabled: true in the widget initialization config:
window.openAIChatManager.initialize({
// ... other config ...
textToSpeechEnabled: true,
ttsVoiceName: 'alloy' // optional voice name override
});
The play icon appears on completed assistant messages when a TTS deployment is available on the server. In Conversation mode, the per-message playback icon is hidden so the live voice exchange is not interrupted by manual playback controls.