Skip to content

Widget

The widget module lets you embed a live AI chat on any website. Visitors interact through a floating chat bubble that connects to Loquent’s text agents over WebSocket. Contact info collection, origin validation, session persistence, and daily message caps are all built in.

The module has four layers:

  1. Admin UI — Dioxus views at /widgets for creating, editing, and managing widget configs
  2. REST API — server functions for CRUD operations on widget configs
  3. Public endpoints — unauthenticated routes that serve the JS bundle and widget config
  4. WebSocket — real-time chat connection between the visitor’s browser and the text agent

The full widget configuration stored in the widget_config table.

pub struct WidgetConfig {
pub id: Uuid,
pub organization_id: Uuid,
pub widget_id: String, // public identifier, e.g. "wid_..."
pub text_agent_id: Uuid,
pub name: Option<String>,
pub greeting: Option<String>,
pub primary_color: String, // default: "#6366f1"
pub text_color: String, // default: "#ffffff"
pub position: WidgetPosition, // bottom_right | bottom_left
pub contact_info_mode: ContactInfoMode, // none | optional | required
pub collect_name: bool,
pub collect_email: bool,
pub collect_phone: bool,
pub allowed_origins: Option<Vec<String>>,
pub daily_message_cap: Option<i32>,
pub enabled: bool,
pub created_at: String,
pub updated_at: String,
}

A safe subset returned to external sites — no org IDs or internal identifiers.

pub struct WidgetPublicConfig {
pub name: Option<String>,
pub greeting: Option<String>,
pub primary_color: String,
pub text_color: String,
pub position: String,
pub contact_info_mode: String,
pub collect_name: bool,
pub collect_email: bool,
pub collect_phone: bool,
}
pub enum WidgetPosition {
BottomRight, // default
BottomLeft,
}

Controls whether visitors must provide contact info before chatting.

pub enum ContactInfoMode {
None, // no collection
Optional, // visitor may provide info
Required, // visitor must provide info before chatting
}

The widget communicates over GET /api/widget/{widget_id}/ws?token=<session_token>.

{ "type": "message", "content": "Hello!" }
{ "type": "contact_info", "name": "Jane", "email": "jane@example.com", "phone": "+1234567890" }
TypeFieldsPurpose
sessionsession_token, contact_info_submittedSent on connect; provides the token for reconnection
historymessages[] (role + content)Replays previous messages on session resume
typing_startAI is generating a response
messagecontent, roleChat message from the assistant
typing_stopAI finished generating
contact_info_savedVisitor contact info was saved
errormessageError description

These routes require no authentication. The widget_id acts as the lookup key.

MethodPathDescription
GET/api/widget/embed.jsServes the compiled widget JS bundle (cached 1 hour)
GET/api/widget/{widget_id}/configReturns WidgetPublicConfig — validates Origin header against allowed origins
GET/api/widget/{widget_id}/wsWebSocket upgrade for live chat

All admin endpoints require session authentication and organization-scoped access.

FunctionDescription
create_widgetCreates a new widget config with a generated wid_* identifier
get_widgetsLists all widget configs for the current organization
get_widgetFetches a single widget config by ID
update_widgetUpdates widget settings (validates input)
delete_widgetDeletes a widget config
reset_widget_idRegenerates the public widget_id (invalidates old embeds)
ServicePurpose
generate_widget_idCreates a unique wid_* identifier
validate_widget_dataValidates WidgetConfigData before create/update
validate_origin / is_origin_allowedChecks the request Origin header against the widget’s allowed origins list
create_widget_sessionCreates a new session row for a visitor
resume_widget_sessionResumes an existing session using the session token
resolve_widget_contactMatches visitor info to an existing contact or creates a stub contact

When a visitor submits contact info, resolve_widget_contact deduplicates against existing contacts:

  1. Look up by email (org-scoped)
  2. Look up by phone (org-scoped)
  3. If both match the same contact → use it
  4. If only one matches → use that contact
  5. If they match different contacts → use the email contact (stronger identifier)
  6. Neither matches → create a new stub contact

Add this script tag to any HTML page:

<script src="https://your-loquent-instance.com/api/widget/embed.js" data-widget="wid_abc123"></script>

The script fetches the widget config, renders a floating chat bubble, and opens a WebSocket connection when the visitor clicks it. Sessions persist across page navigations using the session token stored in the browser.

RouteViewDescription
/widgetsWidgetListViewLists all widgets with status and actions
/widgets/createCreateWidgetViewForm to create a new widget
/widgets/:idWidgetDetailsViewEdit settings, copy embed code, reset widget ID
src/mods/widget/
├── api/ # Server functions (CRUD + reset)
├── components/ # WidgetCardComponent, WidgetFormComponent
├── routes/ # widget_config_route, widget_embed_route, widget_ws_route
├── services/ # Session, contact resolution, validation, ID generation
├── types/ # WidgetConfig, WidgetPublicConfig, WsMessages, enums
└── views/ # List, Create, Details views
widget-js/
├── src/ # Browser-side widget (chat.js, ws.js, styles.js, markdown.js)
├── dist/ # Compiled widget.min.js (served via embed route)
└── build.js # Build script