WA template
This commit is contained in:
parent
7a2e21ba61
commit
d272ae0bd3
12 changed files with 1359 additions and 5 deletions
91
frontend/src/features/agent/AgentTemplateDocsPage.tsx
Normal file
91
frontend/src/features/agent/AgentTemplateDocsPage.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { ArrowLeft, Copy } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { agentTemplatesApi, type AgentTemplateDocumentation } from '../../lib/api'
|
||||
import { API_BASE_URL } from '../../lib/apiBase'
|
||||
|
||||
export function AgentTemplateDocsPage() {
|
||||
const { slug = '' } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [docs, setDocs] = useState<AgentTemplateDocumentation | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
agentTemplatesApi.documentation(slug)
|
||||
.then(setDocs)
|
||||
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load documentation'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [slug])
|
||||
|
||||
if (loading) return <div className="auth-loading">Loading API documentation…</div>
|
||||
if (!docs) return <section className="panel"><p className="auth-error">{error}</p></section>
|
||||
|
||||
const requestExample = JSON.stringify(docs.api.example, null, 2)
|
||||
const curlExample = [
|
||||
`curl -X POST '${API_BASE_URL}${docs.api.path}' \\`,
|
||||
` -H 'Authorization: Bearer urmi_live_<your_api_key>' \\`,
|
||||
` -H 'Content-Type: application/json' \\`,
|
||||
` -d '${requestExample.replace(/'/g, `'\\''`)}'`,
|
||||
].join('\n')
|
||||
|
||||
async function copyExample() {
|
||||
await navigator.clipboard.writeText(requestExample)
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="urmiotp-docs-page">
|
||||
<button className="text-button otp-back" onClick={() => navigate('/console/agent/urmiai')}>
|
||||
<ArrowLeft size={14} /> Back to templates
|
||||
</button>
|
||||
<div className="account-heading">
|
||||
<span className="eyebrow">UrmiAI · {docs.template.slug}</span>
|
||||
<h1>{docs.template.name}</h1>
|
||||
<p>{docs.template.description}</p>
|
||||
</div>
|
||||
|
||||
<section className="panel otp-api-overview">
|
||||
<div className="panel-heading">
|
||||
<div><h2>Send agent WhatsApp message</h2><p>{docs.api.summary}</p></div>
|
||||
<span className="explorer-method">{docs.api.method}</span>
|
||||
</div>
|
||||
<div className="explorer-field-row"><span>Endpoint</span><code>{API_BASE_URL}{docs.api.path}</code></div>
|
||||
<div className="explorer-field-row">
|
||||
<span>Authentication</span>
|
||||
<code>{docs.api.authentication.header}: {docs.api.authentication.format}</code>
|
||||
</div>
|
||||
<div className="otp-parameter-list">
|
||||
{Object.entries(docs.api.parameters).map(([name, description]) => (
|
||||
<div key={name}><code>{name}</code><span>{description}</span></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="otp-code-heading">
|
||||
<strong>Request body</strong>
|
||||
<button className="secondary-button" onClick={copyExample}><Copy size={13} /> {copied ? 'Copied' : 'Copy'}</button>
|
||||
</div>
|
||||
<pre className="explorer-code">{requestExample}</pre>
|
||||
<div className="otp-code-heading"><strong>Example cURL</strong></div>
|
||||
<pre className="explorer-code">{curlExample}</pre>
|
||||
</section>
|
||||
|
||||
<section className="panel agent-template-docs-preview">
|
||||
<div className="panel-heading">
|
||||
<div><h2>Template preview</h2><p>Approved Meta template relayed by Urmi.</p></div>
|
||||
</div>
|
||||
<div className="agent-template-preview card">
|
||||
{docs.template.header_text && <strong>{docs.template.header_text}</strong>}
|
||||
<p>{docs.template.body_preview}</p>
|
||||
{docs.template.footer_text && <small>{docs.template.footer_text}</small>}
|
||||
{docs.template.button_label && <span className="agent-template-button-chip">{docs.template.button_label}</span>}
|
||||
</div>
|
||||
<ul className="agent-template-notes">
|
||||
{docs.notes.map((note) => <li key={note}>{note}</li>)}
|
||||
</ul>
|
||||
<Link to="/console/api-explorer" className="secondary-button">Try in API Explorer</Link>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
248
frontend/src/features/agent/AgentTemplatesPage.tsx
Normal file
248
frontend/src/features/agent/AgentTemplatesPage.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import { ArrowRight, Bot, Code2, Play, X } from 'lucide-react'
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { agentTemplatesApi, type AgentMessageTemplate } from '../../lib/api'
|
||||
|
||||
function TestModal({
|
||||
template,
|
||||
onClose,
|
||||
onSent,
|
||||
}: {
|
||||
template: AgentMessageTemplate
|
||||
onClose: () => void
|
||||
onSent: () => void
|
||||
}) {
|
||||
const [to, setTo] = useState('')
|
||||
const [customerName, setCustomerName] = useState('')
|
||||
const [reportId, setReportId] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [sending, setSending] = useState(false)
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
setError('')
|
||||
setSending(true)
|
||||
try {
|
||||
await agentTemplatesApi.test(template.slug, {
|
||||
to: to.trim(),
|
||||
customer_name: customerName.trim(),
|
||||
report_id: reportId.trim() || undefined,
|
||||
})
|
||||
onSent()
|
||||
onClose()
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'Unable to send test message')
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<form className="modal-card otp-project-modal" onSubmit={submit} onClick={(event) => event.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>Test {template.name}</h2>
|
||||
<button type="button" className="icon-button" onClick={onClose} aria-label="Close"><X size={18} /></button>
|
||||
</div>
|
||||
<p className="agent-template-test-note">
|
||||
One console test per day. Urmi sends from the Urmi AI WhatsApp business number on your behalf.
|
||||
</p>
|
||||
<div className="otp-project-form">
|
||||
<label>
|
||||
<span>Recipient WhatsApp number</span>
|
||||
<input value={to} onChange={(e) => setTo(e.target.value)} placeholder="919149154337" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Customer name</span>
|
||||
<input value={customerName} onChange={(e) => setCustomerName(e.target.value)} placeholder="Suyash" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Report ID (optional)</span>
|
||||
<input value={reportId} onChange={(e) => setReportId(e.target.value)} placeholder="run-abc123" />
|
||||
<small>Used for the dynamic View Report button URL suffix.</small>
|
||||
</label>
|
||||
{error && <p className="auth-error">{error}</p>}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button className="primary-button" disabled={sending}>{sending ? 'Sending…' : 'Send test'}</button>
|
||||
<button type="button" className="secondary-button" onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentTemplatesPage() {
|
||||
const navigate = useNavigate()
|
||||
const [templates, setTemplates] = useState<AgentMessageTemplate[]>([])
|
||||
const [testStatus, setTestStatus] = useState<Record<string, { allowed: boolean; retry_after?: string | null }>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [testTemplate, setTestTemplate] = useState<AgentMessageTemplate | null>(null)
|
||||
const [actionError, setActionError] = useState('')
|
||||
|
||||
async function load() {
|
||||
const items = await agentTemplatesApi.list()
|
||||
setTemplates(items)
|
||||
const statuses = await Promise.all(
|
||||
items
|
||||
.filter((item) => item.subscribed)
|
||||
.map(async (item) => {
|
||||
try {
|
||||
const status = await agentTemplatesApi.testStatus(item.slug)
|
||||
return [item.slug, status] as const
|
||||
} catch {
|
||||
return [item.slug, { allowed: true }] as const
|
||||
}
|
||||
}),
|
||||
)
|
||||
setTestStatus(Object.fromEntries(statuses))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
.catch(() => setError('Unable to load agent templates'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
async function toggleSubscribe(template: AgentMessageTemplate) {
|
||||
setActionError('')
|
||||
try {
|
||||
if (template.subscribed) {
|
||||
await agentTemplatesApi.unsubscribe(template.slug)
|
||||
} else {
|
||||
await agentTemplatesApi.subscribe(template.slug)
|
||||
}
|
||||
await load()
|
||||
} catch (reason) {
|
||||
setActionError(reason instanceof Error ? reason.message : 'Unable to update subscription')
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="auth-loading">Loading agent templates…</div>
|
||||
|
||||
return (
|
||||
<div className="agent-templates-page">
|
||||
<div className="account-heading">
|
||||
<span className="eyebrow">Agent · WhatsApp</span>
|
||||
<h1><Bot size={24} /> UrmiAI agent messages</h1>
|
||||
<p>
|
||||
Subscribe to approved WhatsApp agent templates. Urmi authenticates your API key and sends messages
|
||||
from the Urmi AI business number on behalf of your agents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="panel agent-templates-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>Available templates</h2>
|
||||
<p>Subscribe to enable API access and console testing.</p>
|
||||
</div>
|
||||
<Code2 size={19} />
|
||||
</div>
|
||||
|
||||
{templates.length === 0 ? (
|
||||
<div className="otp-empty">
|
||||
<strong>No agent templates yet</strong>
|
||||
<span>Templates will appear here as Urmi adds them to the platform catalog.</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-template-table">
|
||||
<div className="agent-template-table-head">
|
||||
<span>Template</span>
|
||||
<span>Preview</span>
|
||||
<span>Parameters</span>
|
||||
<span>Status</span>
|
||||
<span />
|
||||
</div>
|
||||
{templates.map((template) => {
|
||||
const status = testStatus[template.slug]
|
||||
const canTest = template.subscribed && (status?.allowed ?? true)
|
||||
return (
|
||||
<div className="agent-template-row" key={template.id}>
|
||||
<div>
|
||||
<strong>{template.name}</strong>
|
||||
<small><code>{template.slug}</code> · Meta: {template.meta_template_name}</small>
|
||||
<small>{template.description}</small>
|
||||
</div>
|
||||
<div className="agent-template-preview">
|
||||
{template.header_text && <strong>{template.header_text}</strong>}
|
||||
<p>{template.body_preview}</p>
|
||||
{template.footer_text && <small>{template.footer_text}</small>}
|
||||
{template.button_label && <span className="agent-template-button-chip">{template.button_label}</span>}
|
||||
</div>
|
||||
<div className="agent-template-params">
|
||||
{template.parameters.map((param) => (
|
||||
<div key={param.name}>
|
||||
<code>{param.name}</code>
|
||||
<span>{param.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<span className={`doc-status ${template.subscribed ? 'approved' : 'pending'}`}>
|
||||
{template.subscribed ? 'Subscribed' : 'Not subscribed'}
|
||||
</span>
|
||||
{template.subscribed && status && !status.allowed && status.retry_after && (
|
||||
<small>Test again after {new Date(status.retry_after).toLocaleString()}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="agent-template-actions">
|
||||
<button
|
||||
className={template.subscribed ? 'secondary-button' : 'primary-button'}
|
||||
onClick={() => toggleSubscribe(template)}
|
||||
>
|
||||
{template.subscribed ? 'Unsubscribe' : 'Subscribe'}
|
||||
</button>
|
||||
{template.subscribed && (
|
||||
<>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={!canTest}
|
||||
onClick={() => setTestTemplate(template)}
|
||||
>
|
||||
<Play size={14} /> Test API
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => navigate(`/console/agent/urmiai/templates/${template.slug}/docs`)}
|
||||
>
|
||||
View docs <ArrowRight size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{(error || actionError) && <p className="auth-error">{error || actionError}</p>}
|
||||
</section>
|
||||
|
||||
<section className="panel agent-templates-help">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>How it works</h2>
|
||||
<p>Urmi acts as your WhatsApp tech provider middle layer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ol className="relay-steps">
|
||||
<li>Subscribe to a template in this console.</li>
|
||||
<li>Create an API key under Develop → API Keys.</li>
|
||||
<li>Call <code>POST /api/v1/agent/messages/send</code> with your Urmi API key.</li>
|
||||
<li>Urmi validates your subscription and relays the message to Meta WhatsApp Cloud API.</li>
|
||||
</ol>
|
||||
<Link to="/console/api-explorer" className="secondary-button">Open API Explorer</Link>
|
||||
</section>
|
||||
|
||||
{testTemplate && (
|
||||
<TestModal
|
||||
template={testTemplate}
|
||||
onClose={() => setTestTemplate(null)}
|
||||
onSent={() => load().catch(() => undefined)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue