UNPKG

configure

Version:

Identity layer SDK for AI agents

1,524 lines (1,355 loc) 52.6 kB
<!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