configure
Version:
Identity layer SDK for AI agents
1,524 lines (1,355 loc) • 52.6 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Configure Agent Template</title>
<script src="https://configure.dev/js/configure.js"></script>
<script src="/brand.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/brand.css">
<style>
:root {
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: "Instrument Serif", Georgia, serif;
--ease: cubic-bezier(0.25, 0.1, 0.25, 1);
--duration: 180ms;
--chat-max: 760px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { min-height: 100%; }
body {
font-family: var(--font-sans);
background: var(--chat-bg);
color: var(--text-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button, textarea { font: inherit; }
button { cursor: pointer; }
a { color: inherit; }
.hidden { display: none !important; }
.home-view {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 360ms var(--ease), transform 360ms var(--ease);
z-index: 2;
}
.home-view.hidden {
opacity: 0;
pointer-events: none;
transform: scale(1.02);
}
.home-bg {
position: absolute;
inset: 0;
background-image: var(--hero-image);
background-size: cover;
background-position: var(--hero-position);
}
.home-bg::after {
content: "";
position: absolute;
inset: 0;
background: var(--hero-overlay);
}
.home-header {
position: absolute;
inset: 0 0 auto 0;
padding: 22px 26px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
z-index: 1;
}
.home-header-left,
.home-header-right {
display: inline-flex;
align-items: center;
gap: 12px;
text-decoration: none;
color: var(--hero-text);
}
.home-header-logo {
height: 20px;
width: auto;
filter: brightness(0) invert(1);
opacity: 0.9;
}
.home-header-link {
font-size: 14px;
color: var(--hero-muted);
text-decoration: none;
transition: color var(--duration) var(--ease);
}
.home-header-link:hover { color: var(--hero-text); }
.home-header-actions {
display: flex;
align-items: center;
gap: 14px;
}
.home-mode-toggle {
background: none;
border: 0;
padding: 0;
cursor: pointer;
white-space: nowrap;
}
.home-center {
position: relative;
z-index: 1;
width: min(100%, 700px);
padding: 32px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
text-align: center;
}
.home-title {
font-family: var(--font-display);
font-size: clamp(3rem, 6vw, 4.75rem);
font-weight: 400;
line-height: 1;
letter-spacing: -0.04em;
color: var(--hero-text);
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.24);
}
.home-subtitle {
max-width: 440px;
font-size: 16px;
line-height: 1.55;
color: var(--hero-muted);
}
.home-form,
.composer-shell {
width: 100%;
display: flex;
align-items: flex-end;
gap: 12px;
padding: 10px 10px 10px 22px;
border-radius: 24px;
border: 1px solid var(--glass-border);
background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(1.2);
-webkit-backdrop-filter: blur(20px) saturate(1.2);
box-shadow: 0 24px 60px rgba(8, 15, 28, 0.18);
}
.home-input,
.composer-input {
flex: 1;
border: 0;
outline: 0;
resize: none;
background: transparent;
color: inherit;
min-height: 26px;
max-height: 140px;
line-height: 1.5;
padding: 10px 0;
}
.home-input {
color: var(--hero-text);
font-size: 18px;
}
.home-input::placeholder { color: rgba(255, 255, 255, 0.62); }
.send-btn {
flex: 0 0 auto;
width: 46px;
height: 46px;
border: 0;
border-radius: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--send-bg);
color: var(--send-fg);
opacity: 0;
transition: transform var(--duration) var(--ease), background var(--duration) var(--ease), opacity var(--duration) var(--ease);
}
.home-form.has-value .send-btn,
.composer-shell.has-value .send-btn {
opacity: 1;
}
.send-btn:hover:not(:disabled) { transform: translateY(-1px); }
.send-btn:disabled { opacity: 0.42; cursor: not-allowed; }
.home-suggestions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
}
.suggestion-btn,
.action-chip,
.status-pill,
.tool-pill,
.logout-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 36px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.suggestion-btn {
color: var(--hero-text);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.22);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.suggestion-btn:hover { background: rgba(255, 255, 255, 0.14); }
.home-footer {
position: absolute;
inset: auto 0 18px 0;
text-align: center;
z-index: 1;
font-size: 12px;
color: rgba(255, 255, 255, 0.54);
}
.chat-view {
min-height: 100vh;
display: none;
background:
radial-gradient(circle at top left, rgba(74, 158, 255, 0.08), transparent 28%),
linear-gradient(180deg, #ffffff 0%, var(--chat-bg) 100%);
}
.chat-view.active { display: block; }
.chat-header {
position: sticky;
top: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 16px 20px;
border-bottom: 1px solid var(--chat-border);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.chat-brand {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
color: var(--text-secondary);
font-size: 14px;
}
.chat-brand-mark {
width: 18px;
height: 18px;
border-radius: 0;
overflow: visible;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
color: #667085;
flex-shrink: 0;
box-shadow: none;
}
.chat-brand-mark img {
width: 18px;
height: 18px;
object-fit: contain;
display: block;
}
.chat-brand-copy {
min-width: 0;
display: flex;
align-items: center;
flex-direction: row;
gap: 10px;
}
.chat-brand-title,
.chat-brand-subtitle {
font-size: 14px;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-brand-title {
font-weight: 600;
color: var(--text-secondary);
}
.chat-brand-subtitle {
color: var(--text-primary);
}
.chat-brand-slash {
color: #d0d5dd;
font-size: 14px;
line-height: 1;
}
.chat-header-right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
.status-pill,
.tool-pill,
.logout-btn,
.action-chip,
.reset-btn,
.chat-mode-toggle {
border-color: var(--chat-border-strong);
background: rgba(255, 255, 255, 0.9);
color: var(--text-secondary);
}
.status-pill.ready,
.tool-pill.complete {
color: var(--success);
border-color: rgba(34, 197, 94, 0.2);
background: rgba(34, 197, 94, 0.08);
}
.status-pill.warn,
.tool-pill.running {
color: var(--warning);
border-color: rgba(245, 158, 11, 0.24);
background: rgba(245, 158, 11, 0.1);
}
.tool-pill.error,
.runtime-banner.error,
.auth-error {
color: var(--error);
border-color: rgba(239, 68, 68, 0.22);
background: rgba(239, 68, 68, 0.08);
}
.logout-btn,
.reset-btn,
.chat-mode-toggle {
border: 1px solid var(--chat-border);
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.78);
padding: 0 16px;
min-height: 34px;
font-size: 13px;
font-weight: 500;
}
.chat-main {
width: min(100%, 760px);
margin: 0 auto;
padding: 24px 24px 32px;
display: flex;
flex-direction: column;
gap: 18px;
min-height: calc(100vh - 72px);
}
.runtime-banner {
border-radius: 16px;
border: 1px solid var(--chat-border-strong);
padding: 14px 16px;
font-size: 13px;
line-height: 1.5;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.86);
}
.thread {
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
padding-bottom: 8px;
}
.landing {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
text-align: center;
padding: 64px 0 18px;
}
.landing.hidden { display: none; }
.landing-title {
font-family: var(--font-display);
font-size: clamp(2.2rem, 4.4vw, 3.4rem);
font-weight: 400;
line-height: 1;
letter-spacing: -0.04em;
}
.landing-copy {
max-width: 540px;
font-size: 15px;
line-height: 1.6;
color: var(--text-secondary);
}
.landing-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.action-chip:hover,
.logout-btn:hover {
background: rgba(17, 24, 39, 0.08);
}
.messages {
display: flex;
flex-direction: column;
gap: 18px;
min-height: 0;
}
.message-row {
display: flex;
width: 100%;
}
.message-row.user,
.message-row.assistant,
.message-row.surface,
.message-row.auth { justify-content: flex-start; }
.message-row.focal { justify-content: center; }
.message-bubble {
max-width: min(100%, 560px);
padding: 16px 18px;
border-radius: 18px;
border: 1px solid var(--chat-border);
background: var(--assistant-bubble);
color: var(--text-primary);
line-height: 1.6;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
white-space: pre-wrap;
word-break: break-word;
}
.message-row.user .message-bubble {
color: var(--text-primary);
background: rgba(17, 24, 39, 0.06);
border-color: transparent;
min-width: min(100%, 560px);
box-shadow: none;
}
.message-row.focal .message-bubble {
min-width: min(100%, 550px);
max-width: min(100%, 550px);
padding: 18px 20px;
border-radius: 18px;
}
.message-row.plain .message-bubble {
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
color: var(--text-secondary);
font-size: 16px;
line-height: 1.55;
border-radius: 0;
margin-left: 40px;
}
.message-row.surface .message-bubble {
width: 100%;
max-width: 100%;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.auth-row {
width: min(100%, 420px);
display: flex;
flex-direction: column;
gap: 12px;
margin: 0 auto;
}
.auth-row.hidden { display: none; }
.auth-frame-shell,
.surface-card {
width: 100%;
border-radius: 28px;
border: 1px solid var(--chat-border);
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 20px 46px rgba(15, 23, 42, 0.06);
overflow: hidden;
}
.surface-card-head {
padding: 20px 20px 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.surface-card-title {
font-size: 20px;
line-height: 1.1;
font-weight: 600;
}
.surface-card-copy {
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
}
.auth-error {
margin: 12px 12px 0;
border-radius: 14px;
border: 1px solid rgba(239, 68, 68, 0.22);
padding: 12px 14px;
font-size: 13px;
line-height: 1.5;
display: none;
}
.auth-surface {
min-height: 520px;
overflow: hidden;
background: #fff;
}
.composer-wrap {
position: sticky;
bottom: 0;
padding-top: 8px;
background: linear-gradient(180deg, rgba(246, 247, 249, 0) 0%, rgba(246, 247, 249, 0.92) 22%, rgba(246, 247, 249, 1) 100%);
}
.composer-wrap.hidden { display: none; }
.composer-shell {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.94);
border-color: var(--chat-border-strong);
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.1);
}
.composer-input::placeholder { color: var(--text-placeholder); }
.surface-modal {
position: fixed;
inset: 0;
z-index: 20;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(10, 14, 24, 0.32);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.surface-modal.active { display: flex; }
.surface-modal-dialog {
width: min(100%, 760px);
max-height: min(88vh, 860px);
border-radius: 28px;
border: 1px solid var(--chat-border);
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 40px 100px rgba(15, 23, 42, 0.18);
display: flex;
flex-direction: column;
overflow: hidden;
}
.surface-modal-topbar {
padding: 18px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--chat-border);
}
.surface-modal-copy {
display: flex;
flex-direction: column;
gap: 4px;
}
.surface-modal-title {
font-size: 16px;
font-weight: 600;
}
.surface-modal-subtitle {
font-size: 13px;
color: var(--text-secondary);
}
.surface-close {
width: 40px;
height: 40px;
border-radius: 14px;
border: 0;
color: var(--text-primary);
background: rgba(15, 23, 42, 0.06);
}
.surface-modal-body {
padding: 18px 20px 20px;
overflow: auto;
}
.surface-inline {
border-radius: 20px;
border: 1px solid var(--chat-border);
background: rgba(255, 255, 255, 0.96);
padding: 14px;
}
.tool-status {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tool-status:empty { display: none; }
@media (max-width: 860px) {
.chat-header {
padding: 16px;
align-items: flex-start;
}
.chat-header-right {
width: auto;
justify-content: flex-start;
}
.chat-main {
width: 100%;
padding: 18px 16px 24px;
}
.chat-brand-copy {
gap: 8px;
}
.home-center {
padding: 24px 16px;
}
.home-form,
.composer-shell {
padding-left: 16px;
}
}
@media (max-width: 640px) {
.home-header {
padding: 18px 16px;
}
.home-header-right {
display: none;
}
.home-title {
font-size: clamp(2.6rem, 15vw, 3.3rem);
}
.home-subtitle {
font-size: 15px;
}
.home-form,
.composer-shell {
gap: 10px;
border-radius: 22px;
}
.home-input,
.composer-input {
font-size: 16px;
}
.send-btn {
width: 42px;
height: 42px;
border-radius: 14px;
}
.message-bubble {
max-width: 100%;
}
.surface-modal {
padding: 12px;
}
.surface-modal-dialog {
width: 100%;
max-height: calc(100vh - 24px);
border-radius: 24px;
}
.home-header-actions {
gap: 10px;
}
}
</style>
</head>
<body>
<main id="home-view" class="home-view">
<div class="home-bg"></div>
<header class="home-header">
<a class="home-header-left" id="home-brand-link" href="https://configure.dev/skill.md" target="_blank" rel="noreferrer">
<img src="https://configure.dev/public/configure-lockup.svg" alt="Configure" class="home-header-logo">
</a>
<div class="home-header-actions">
<button id="home-mode-toggle" class="home-header-link home-mode-toggle" type="button" aria-label="Switch from dark mode to light mode" aria-pressed="false">Light mode</button>
<a class="home-header-link home-header-right" id="home-top-link" href="https://configure.dev/skill.md" target="_blank" rel="noreferrer">skill.md</a>
</div>
</header>
<section class="home-center">
<h1 id="home-title" class="home-title">Your Agent</h1>
<p id="home-subtitle" class="home-subtitle">Secure, profile-aware help in one thread.</p>
<form id="home-form" class="home-form">
<textarea id="home-input" class="home-input" rows="1" autocomplete="off" placeholder="Ask your agent anything..."></textarea>
<button id="home-send" class="send-btn" type="submit" aria-label="Send first message">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M3.5 9h10.5M9 3.5 14.5 9 9 14.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</form>
<div id="home-suggestions" class="home-suggestions"></div>
</section>
<div id="home-footer" class="home-footer">powered by configure</div>
</main>
<section id="chat-view" class="chat-view" aria-live="polite">
<header class="chat-header">
<div class="chat-brand">
<div id="chat-brand-mark" class="chat-brand-mark" aria-hidden="true"></div>
<div class="chat-brand-copy">
<div id="chat-brand-title" class="chat-brand-title">configure</div>
<div class="chat-brand-slash" aria-hidden="true">/</div>
<div id="chat-brand-subtitle" class="chat-brand-subtitle">your-agent</div>
</div>
</div>
<div class="chat-header-right">
<span id="runtime-pill" class="status-pill hidden">Runtime pending</span>
<span id="session-pill" class="status-pill hidden">Signed out</span>
<button id="chat-mode-toggle" class="chat-mode-toggle" type="button" aria-label="Switch from dark mode to light mode" aria-pressed="false">Light mode</button>
<button id="reset-btn" class="reset-btn" type="button">new chat</button>
<button id="logout-btn" class="logout-btn hidden" type="button">log out</button>
</div>
</header>
<div class="chat-main">
<div id="runtime-banner" class="runtime-banner hidden"></div>
<div id="tool-status" class="tool-status"></div>
<section id="landing" class="landing">
<h2 id="landing-title" class="landing-title">How can I help?</h2>
<p id="landing-copy" class="landing-copy">Ask your first question below. Configure will handle sign-in inline if needed.</p>
<div id="landing-actions" class="landing-actions"></div>
</section>
<section id="thread" class="thread">
<div id="messages" class="messages"></div>
<div id="auth-row" class="auth-row hidden">
<div id="auth-error" class="auth-error"></div>
<div class="auth-frame-shell">
<div id="auth-surface" class="auth-surface"></div>
</div>
</div>
</section>
<div class="composer-wrap">
<form id="composer" class="composer-shell">
<textarea id="chat-input" class="composer-input" rows="1" autocomplete="off" placeholder="Ask your agent anything..."></textarea>
<button id="chat-send" class="send-btn" type="submit" aria-label="Send message">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M3.5 9h10.5M9 3.5 14.5 9 9 14.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</form>
</div>
</div>
</section>
<aside id="surface-modal" class="surface-modal" aria-hidden="true">
<div class="surface-modal-dialog">
<div class="surface-modal-topbar">
<div class="surface-modal-copy">
<div id="surface-modal-title" class="surface-modal-title">Configure surface</div>
<div id="surface-modal-subtitle" class="surface-modal-subtitle">Hosted by Configure</div>
</div>
<button id="surface-close" class="surface-close" type="button" aria-label="Close">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M4.5 4.5 13.5 13.5M13.5 4.5 4.5 13.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</button>
</div>
<div id="surface-modal-body" class="surface-modal-body"></div>
</div>
</aside>
<script>
(async function () {
const STORAGE_TOKEN = 'configure_template_token';
const STORAGE_USER = 'configure_template_user';
const STORAGE_PHONE = 'configure_template_phone';
const STORAGE_UI_MODE = 'configure_template_ui_mode';
const STORAGE_CONVERSATION = 'configure_template_conversation';
const brand = window.CONFIGURE_BRAND || {};
const state = {
config: null,
token: localStorage.getItem(STORAGE_TOKEN) || '',
userId: localStorage.getItem(STORAGE_USER) || '',
phone: localStorage.getItem(STORAGE_PHONE) || '',
uiMode: localStorage.getItem(STORAGE_UI_MODE) || brand.defaultMode || 'dark',
conversationId: localStorage.getItem(STORAGE_CONVERSATION) || '',
authed: Boolean(localStorage.getItem(STORAGE_TOKEN) && localStorage.getItem(STORAGE_USER)),
sending: false,
authMounted: false,
pendingInput: '',
pendingFromUserBubble: false,
history: [],
activeMessageCount: 0,
userName: '',
};
function createConversationId() {
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
function ensureConversationId() {
if (!state.conversationId) {
state.conversationId = createConversationId();
localStorage.setItem(STORAGE_CONVERSATION, state.conversationId);
}
return state.conversationId;
}
function resetConversationId() {
state.conversationId = createConversationId();
localStorage.setItem(STORAGE_CONVERSATION, state.conversationId);
}
const homeView = document.getElementById('home-view');
const chatView = document.getElementById('chat-view');
const homeTitle = document.getElementById('home-title');
const homeSubtitle = document.getElementById('home-subtitle');
const homeForm = document.getElementById('home-form');
const homeInput = document.getElementById('home-input');
const homeSend = document.getElementById('home-send');
const homeSuggestions = document.getElementById('home-suggestions');
const homeBrandLink = document.getElementById('home-brand-link');
const homeTopLink = document.getElementById('home-top-link');
const homeModeToggle = document.getElementById('home-mode-toggle');
const homeFooter = document.getElementById('home-footer');
const chatBrandMark = document.getElementById('chat-brand-mark');
const chatBrandTitle = document.getElementById('chat-brand-title');
const chatBrandSubtitle = document.getElementById('chat-brand-subtitle');
const runtimePill = document.getElementById('runtime-pill');
const sessionPill = document.getElementById('session-pill');
const chatModeToggle = document.getElementById('chat-mode-toggle');
const resetBtn = document.getElementById('reset-btn');
const logoutBtn = document.getElementById('logout-btn');
const runtimeBanner = document.getElementById('runtime-banner');
const toolStatus = document.getElementById('tool-status');
const landing = document.getElementById('landing');
const landingTitle = document.getElementById('landing-title');
const landingCopy = document.getElementById('landing-copy');
const landingActions = document.getElementById('landing-actions');
const messages = document.getElementById('messages');
const authRow = document.getElementById('auth-row');
const authError = document.getElementById('auth-error');
const authSurface = document.getElementById('auth-surface');
const composerWrap = document.querySelector('.composer-wrap');
const composer = document.getElementById('composer');
const chatInput = document.getElementById('chat-input');
const chatSend = document.getElementById('chat-send');
const surfaceModal = document.getElementById('surface-modal');
const surfaceModalTitle = document.getElementById('surface-modal-title');
const surfaceModalSubtitle = document.getElementById('surface-modal-subtitle');
const surfaceModalBody = document.getElementById('surface-modal-body');
const surfaceClose = document.getElementById('surface-close');
function currentConfigureTheme() {
const byMode = brand.configureThemeByMode || {};
return byMode[state.uiMode] || brand.configureTheme || 'light';
}
function applyUiMode() {
document.documentElement.dataset.uiMode = state.uiMode;
document.documentElement.style.colorScheme = state.uiMode;
const nextMode = state.uiMode === 'dark' ? 'light' : 'dark';
const currentLabel = state.uiMode === 'dark' ? 'Dark mode' : 'Light mode';
const nextLabel = nextMode === 'dark' ? 'Dark mode' : 'Light mode';
[homeModeToggle, chatModeToggle].forEach(button => {
if (!button) return;
button.textContent = nextLabel;
button.setAttribute('aria-label', `Switch from ${currentLabel.toLowerCase()} to ${nextLabel.toLowerCase()}`);
button.setAttribute('title', `Currently ${currentLabel.toLowerCase()}. Switch to ${nextLabel.toLowerCase()}.`);
button.setAttribute('aria-pressed', state.uiMode === 'light' ? 'true' : 'false');
});
}
function toggleUiMode() {
state.uiMode = state.uiMode === 'dark' ? 'light' : 'dark';
localStorage.setItem(STORAGE_UI_MODE, state.uiMode);
applyUiMode();
if (!authRow.classList.contains('hidden')) {
authSurface.innerHTML = '';
state.authMounted = false;
ensureAuthMounted();
}
}
function humanize(value) {
return String(value || 'agent')
.replace(/[-_]+/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase())
.trim();
}
function fillTemplate(value, agentName) {
return String(value || '').replace(/\{agentName\}/g, agentName);
}
function currentAgentName() {
const config = state.config || {};
return config.agentName || brand.title || humanize(config.agent) || 'Your Agent';
}
function currentBrandTitle() {
return fillTemplate(brand.title || state.config?.agentName || 'Your Agent', currentAgentName());
}
function currentSubtitle() {
return fillTemplate(brand.subtitle || 'Secure, profile-aware help in one thread.', currentAgentName());
}
function currentHomeTitle() {
return fillTemplate(brand.homeTitle || currentBrandTitle(), currentAgentName());
}
function currentHomeSubtitle() {
return fillTemplate(brand.homeSubtitle || currentSubtitle(), currentAgentName());
}
function currentHomePlaceholder() {
return fillTemplate(brand.homePlaceholder || 'Ask your agent anything...', currentAgentName());
}
function currentChatPlaceholder() {
return fillTemplate(brand.chatPlaceholder || brand.composerPlaceholder || 'Ask your agent anything...', currentAgentName());
}
function currentLandingTitle() {
if (!state.authed) {
return fillTemplate(brand.authLandingTitle || 'Your first message is ready.', currentAgentName());
}
if (state.userName) {
return fillTemplate(brand.signedInTitle || 'Hi, {agentName}.', state.userName);
}
return fillTemplate(brand.signedInTitle || 'How can I help?', currentAgentName());
}
function currentLandingCopy() {
if (!state.authed) {
return fillTemplate(brand.introCopy || 'Ask your first question below. Configure will handle sign-in inline if needed.', currentAgentName());
}
if (!state.config?.runtimeReady) {
return fillTemplate(brand.runtimeMissingCopy || state.config.runtimeMessage || '', currentAgentName());
}
return fillTemplate(brand.signedInPrompt || 'You are linked. Tell me what you need help with.', currentAgentName());
}
function applyBrand() {
const agentName = currentAgentName();
const brandTitle = currentBrandTitle();
const subtitle = currentSubtitle();
document.title = fillTemplate(brand.pageTitle || `${agentName} | Configure Template`, agentName);
homeTitle.textContent = currentHomeTitle();
homeSubtitle.textContent = currentHomeSubtitle();
homeInput.placeholder = currentHomePlaceholder();
chatInput.placeholder = currentChatPlaceholder();
chatBrandTitle.textContent = brand.homeLockupLabel || 'configure';
chatBrandSubtitle.textContent = String(state.config?.agent || agentName).toLowerCase();
landingTitle.textContent = currentLandingTitle();
landingCopy.textContent = currentLandingCopy();
homeFooter.textContent = fillTemplate(brand.footerText || 'powered by configure', agentName);
if (brand.heroImageUrl) {
document.documentElement.style.setProperty('--hero-image', `url("${brand.heroImageUrl}")`);
}
if (brand.heroImagePosition) {
document.documentElement.style.setProperty('--hero-position', brand.heroImagePosition);
}
if (brand.accentColor) {
document.documentElement.style.setProperty('--accent', brand.accentColor);
}
if (brand.logoUrl) {
chatBrandMark.innerHTML = `<img src="${brand.logoUrl}" alt="">`;
} else {
chatBrandMark.innerHTML = '<img src="https://configure.dev/public/brand/configure-logo.svg" alt="">';
}
if (brand.homeLinkUrl) {
homeBrandLink.href = brand.homeLinkUrl;
homeTopLink.href = brand.homeLinkUrl;
}
homeTopLink.textContent = brand.homeLinkLabel || 'configure.dev';
renderHomeSuggestions();
renderLandingActions();
}
function renderHomeSuggestions() {
const prompts = Array.isArray(brand.samplePrompts) ? brand.samplePrompts : [];
homeSuggestions.innerHTML = '';
prompts.forEach(prompt => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'suggestion-btn';
button.textContent = prompt;
button.addEventListener('click', () => {
homeInput.value = prompt;
autoResize(homeInput);
updateSendState();
homeInput.focus();
});
homeSuggestions.appendChild(button);
});
}
function renderLandingActions() {
const agentName = currentAgentName();
landingActions.innerHTML = '';
const actions = [
{ key: 'connections', label: fillTemplate(brand.actionLabels?.connections || 'Connect apps', agentName), title: 'Connections', copy: 'Link Gmail, Calendar, Drive, Notion, and other connectors through Configure.' },
{ key: 'memoryImport', label: fillTemplate(brand.actionLabels?.memoryImport || 'Import memories', agentName), title: 'Memory import', copy: 'Bring prior conversation context from other assistants into Configure.' },
{ key: 'profile', label: fillTemplate(brand.actionLabels?.profile || 'Edit profile', agentName), title: 'Profile', copy: 'Review and refine the profile this agent uses for personalization.' },
];
actions.forEach(action => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'action-chip';
button.textContent = action.label;
button.disabled = !state.authed;
if (!state.authed) button.classList.add('hidden');
button.addEventListener('click', () => openUtilitySurface(action));
landingActions.appendChild(button);
});
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 140) + 'px';
}
function updateSendState() {
const homeHasValue = Boolean(homeInput.value.trim());
const chatHasValue = Boolean(chatInput.value.trim());
const authPending = !state.authed && Boolean(state.pendingInput);
homeSend.disabled = state.sending || !homeHasValue;
chatSend.disabled = state.sending || !chatHasValue || authPending;
homeForm.classList.toggle('has-value', homeHasValue);
composer.classList.toggle('has-value', chatHasValue);
composerWrap.classList.toggle('hidden', authPending);
if (authPending) {
chatInput.placeholder = fillTemplate(brand.authPendingPlaceholder || 'Complete sign-in to continue your first message...', currentAgentName());
} else {
chatInput.placeholder = currentChatPlaceholder();
}
}
function updateRuntimeState() {
if (!state.config) return;
runtimePill.textContent = state.config.runtimeReady ? 'Runtime ready' : 'Runtime setup needed';
runtimePill.className = `status-pill hidden ${state.config.runtimeReady ? 'ready' : 'warn'}`;
runtimeBanner.textContent = state.config.runtimeMessage;
runtimeBanner.className = `runtime-banner${state.config.runtimeReady ? ' hidden' : ''}`;
}
function updateSessionState() {
if (state.authed) {
sessionPill.textContent = 'Signed in';
sessionPill.className = 'status-pill ready hidden';
logoutBtn.classList.remove('hidden');
} else {
sessionPill.textContent = 'Signed out';
sessionPill.className = 'status-pill hidden';
logoutBtn.classList.add('hidden');
}
renderLandingActions();
landingTitle.textContent = currentLandingTitle();
landingCopy.textContent = currentLandingCopy();
updateSendState();
}
function setChatView() {
homeView.classList.add('hidden');
chatView.classList.add('active');
}
function setHomeView() {
chatView.classList.remove('active');
homeView.classList.remove('hidden');
}
function updateLandingVisibility() {
const hasMessages = state.activeMessageCount > 0 || authRow.classList.contains('hidden') === false;
landing.classList.toggle('hidden', hasMessages);
}
function appendMessage(role, text, variant = 'bubble') {
const row = document.createElement('div');
row.className = `message-row ${role}${variant === 'plain' ? ' plain' : ''}`;
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
bubble.textContent = text;
row.appendChild(bubble);
messages.appendChild(row);
state.activeMessageCount += 1;
updateLandingVisibility();
row.scrollIntoView({ block: 'end' });
return bubble;
}
function appendSurfaceCard(title, copy) {
const row = document.createElement('div');
row.className = 'message-row surface';
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
const card = document.createElement('div');
card.className = 'surface-card';
const head = document.createElement('div');
head.className = 'surface-card-head';
const cardTitle = document.createElement('h3');
cardTitle.className = 'surface-card-title';
cardTitle.textContent = title;
const cardCopy = document.createElement('p');
cardCopy.className = 'surface-card-copy';
cardCopy.textContent = copy || 'Hosted by Configure';
const body = document.createElement('div');
body.className = 'surface-inline';
head.append(cardTitle, cardCopy);
card.append(head, body);
bubble.appendChild(card);
row.appendChild(bubble);
messages.appendChild(row);
state.activeMessageCount += 1;
updateLandingVisibility();
row.scrollIntoView({ block: 'end' });
return body;
}
function clearThread() {
messages.innerHTML = '';
toolStatus.innerHTML = '';
authRow.classList.add('hidden');
authError.style.display = 'none';
authError.textContent = '';
state.history = [];
state.pendingInput = '';
state.pendingFromUserBubble = false;
state.activeMessageCount = 0;
resetConversationId();
updateLandingVisibility();
}
function queueFirstPrompt(text) {
setChatView();
sendMessage(text);
}
function showAuthRow() {
authRow.classList.remove('hidden');
updateLandingVisibility();
ensureAuthMounted();
}
function hideAuthRow() {
authRow.classList.add('hidden');
updateLandingVisibility();
}
function ensureAuthMounted() {
if (state.authMounted || typeof Configure === 'undefined' || !state.config) return;
Configure.link({
el: '#auth-surface',
publishableKey: state.config.publishableKey,
agent: state.config.agent,
agentName: currentAgentName(),
theme: currentConfigureTheme(),
});
state.authMounted = true;
}
function setToolStatus(tool, status) {
if (!tool) return;
const id = `tool-${tool.replace(/[^a-z0-9]/gi, '-')}`;
let pill = document.getElementById(id);
if (!pill) {
pill = document.createElement('span');
pill.id = id;
pill.className = 'tool-pill';
toolStatus.appendChild(pill);
}
pill.className = `tool-pill ${status}`;
pill.textContent = `${humanize(tool)} ${status}`;
if (status === 'complete') {
setTimeout(() => {
if (pill.parentNode) pill.remove();
}, 1800);
}
}
function openSurfaceModal(title, subtitle) {
surfaceModalTitle.textContent = title;
surfaceModalSubtitle.textContent = subtitle || 'Hosted by Configure';
surfaceModalBody.innerHTML = '';
surfaceModal.classList.add('active');
surfaceModal.setAttribute('aria-hidden', 'false');
const target = document.createElement('div');
surfaceModalBody.appendChild(target);
return target;
}
function closeSurfaceModal() {
surfaceModal.classList.remove('active');
surfaceModal.setAttribute('aria-hidden', 'true');
surfaceModalBody.innerHTML = '';
}
function commonSurfaceProps(target, authed) {
const base = {
el: target,
publishableKey: state.config.publishableKey,
agent: state.config.agent,
agentName: currentAgentName(),
theme: currentConfigureTheme(),
};
return authed ? Object.assign(base, { token: state.token, userId: state.userId }) : base;
}
function mountConfigureSurface(component, target, props) {
if (typeof Configure === 'undefined') {
target.innerHTML = '<div class="runtime-banner error">Failed to load Configure. Refresh the page and try again.</div>';
return;
}
const joinedConnectors = Array.isArray(props.connectors) ? props.connectors.join(',') : props.connectors;
switch (component) {
case 'connections':
case 'connection_list':
Configure.connections(Object.assign(commonSurfaceProps(target, true), { tools: joinedConnectors || 'gmail,calendar,drive,notion' }));
return;
case 'single_connector':
Configure.singleConnector(Object.assign(commonSurfaceProps(target, true), {
tool: props.tool || 'gmail',
message: props.message,
name: props.name,
}));
return;
case 'memory_import':
Configure.memoryImport(Object.assign(commonSurfaceProps(target, true), { providers: props.providers || 'chatgpt,claude,gemini,grok' }));
return;
case 'profile':
case 'profile_editor':
Configure.profileEditor(commonSurfaceProps(target, true));
return;
case 'memory_card':
Configure.memoryCard(Object.assign(commonSurfaceProps(target, false), { memories: props.memories || [] }));
return;
case 'confirmation':
Configure.confirmation(Object.assign(commonSurfaceProps(target, false), {
message: props.message || 'Continue?',
confirmLabel: props.confirm_label || props.confirmLabel || 'Confirm',
cancelLabel: props.cancel_label || props.cancelLabel || 'Cancel',
}));
return;
case 'access_request':
Configure.accessRequest(Object.assign(commonSurfaceProps(target, true), {
tool: props.tool || 'gmail',
message: props.message,
description: props.description,
}));
return;
case 'tool_approval':
Configure.toolApproval(Object.assign(commonSurfaceProps(target, false), {
tool: props.tool || 'gmail',
params: props.params || {},
actionId: props.actionId || props.action_id || crypto.randomUUID(),
timeoutSeconds: props.timeoutSeconds || props.timeout_seconds,
}));
return;
default:
target.innerHTML = `<pre>${JSON.stringify({ component, props }, null, 2)}</pre>`;
}
}
function openUtilitySurface(action) {
if (!state.authed) return showAuthRow();
const target = openSurfaceModal(action.title, action.copy);
if (action.key === 'connections') {
mountConfigureSurface('connections', target, { tools: brand.defaultTools || 'gmail,calendar,drive,notion' });
return;
}
if (action.key === 'memoryImport') {
mountConfigureSurface('memory_import', target, { providers: brand.defaultMemoryProviders || 'chatgpt,claude,gemini,grok' });
return;
}
mountConfigureSurface('profile_editor', target, {});
}
function mountUiComponent(component, props) {
const target = appendSurfaceCard(humanize(component), 'Hosted by Configure').closest('.message-row').querySelector('.surface-inline');
mountConfigureSurface(component, target, props || {});
}
async function loadName() {
if (!state.token || !state.userId) return '';
try {
const res = await fetch('/api/hello', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: state.token, userId: state.userId }),
});
const data = await res.json();
return data.name || '';
} catch {
return '';
}
}
function handleAuthed() {
state.authed = true;
updateSessionState();
hideAuthRow();
landingTitle.textContent = currentLandingTitle();
landingCopy.textContent = currentLandingCopy();
if (!state.config.runtimeReady && state.pendingInput) {
appendMessage('assistant', state.config.runtimeMessage);
state.history.push({ role: 'user', content: state.pendingInput }, { role: 'assistant', content: state.config.runtimeMessage });
state.pendingInput = '';
state.pendingFromUserBubble = false;
return;
}
if (state.pendingInput) {
const next = state.pendingInput;
state.pendingInput = '';
const skipUserBubble = state.pendingFromUserBubble;
state.pendingFromUserBubble = false;
sendMessage(next, { skipUserBubble });
}
}
async function sendMessage(text, options = {}) {
if (!text || state.sending) return;
if (!state.authed) {
setChatView();
if (!options.skipUserBubble) {
appendMessage('user', text);
appendMessage('assistant', fillTemplate(brand.authLeadCopy || 'Let\'s link your phone to get started.', currentAgentName()), 'plain');
}
state.pendingInput = text;
state.pendingFromUserBubble = true;
showAuthRow();
updateSendState();
return;
}
if (!state.config.runtimeReady) {
appendMessage('assistant', state.config.runtimeMessage);
return;
}
setChatView();
state.sending = true;
updateSendState();
if (!options.skipUserBubble) appendMessage('user', text);
landing.classList.add('hidden');
const assistantBubble = appendMessage('assistant', '');
let assistantText = '';
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: state.token, userId: state.userId, message: text, history: state.history, conversationId: ensureConversationId() }),
});
if (!res.ok || !res.body) {
throw new Error('Chat request failed');
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n\n');
buffer = parts.pop() || '';
for (const part of parts) {
const line = part.split('\n').find(item => item.startsWith('data:'));
if (!line) continue;
const event = JSON.parse(line.slice(5));
if (event.type === 'delta') {
assistantText += event.text;
assistantBubble.textContent = assistantText;
assistantBubble.scrollIntoView({ block: 'end' });
} else if (event.type === 'tool_status') {
setToolStatus(event.tool, event.status);
} else if (event.type === 'ui_component') {
mountUiComponent(event.component, event.props || {});
} else if (event.type === 'error') {
assistantBubble.textContent = event.message;
assistantText = event.message;
}
}
}
} catch (err) {
assistantText = `Error: ${err.message}`;
assistantBubble.textContent = assistantText;
} finally {
if (assistantText) {
state.history.push({ role: 'user', content: text }, { role: 'assistant', content: assistantText });
}
state.sending = false;
chatInput.value = '';
autoResize(chatInput);
updateSendState();
chatInput.focus();
}
}
homeForm.addEventListener('submit', event => {
event.preventDefault();
const text = homeInput.value.trim();
if (!text) return;
homeInput.value = '';
autoResize(homeInput);
updateSendState();
queueFirstPrompt(text);
});
composer.addEventListener('submit', event => {
event.preventDefault();
const text = chatInput.value.trim();
if (!text) return;
sendMessage(text);
});
[homeInput, chatInput].forEach(textarea => {
textarea.addEventListener('input', () => {
autoResize(textarea);
updateSendState();
});
textarea.addEventListener('keydown', event => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (textarea === homeInput) {
homeForm.requestS