Add Super Admin AI prompt templates and business assignment UI.

Templates library plus per-business prompt list with edit, copy public link, and remove.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 15:28:41 +03:30
co-authored by Cursor
parent b50e4fd812
commit 0aab7b5529
13 changed files with 1209 additions and 132 deletions
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
AlertTriangle,
Copy,
Database,
FileText,
Globe,
@@ -10,6 +11,7 @@ import {
Plus,
RotateCcw,
Search,
Trash2,
UserCog,
} from 'lucide-react'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
@@ -46,6 +48,14 @@ import {
} from '../services/businessService'
import { listBusinessCategories } from '../services/categoryService'
import { listUserTitles, type UserTitleOption } from '../services/userTitleService'
import {
createDomainAiPrompt,
deleteDomainAiPrompt,
listAiPromptTemplates,
listDomainAiPrompts,
updateDomainAiPrompt,
} from '../services/aiPromptService'
import type { AiPromptTemplate, DomainAiPrompt } from '../types/aiPrompt'
import { Modal } from '../components/Modal'
import { PrimaryColorSwatchControl } from '../components/PrimaryColorSwatchControl'
import { useToast } from '../context/ToastContext'
@@ -315,6 +325,21 @@ export function BusinessesPage() {
const [domainSubmitting, setDomainSubmitting] = useState(false)
const [domainError, setDomainError] = useState('')
const [promptsOpen, setPromptsOpen] = useState(false)
const [promptsBusiness, setPromptsBusiness] = useState<BusinessListItem | null>(null)
const [domainPrompts, setDomainPrompts] = useState<DomainAiPrompt[]>([])
const [allPromptTemplates, setAllPromptTemplates] = useState<AiPromptTemplate[]>([])
const [promptsLoading, setPromptsLoading] = useState(false)
const [promptsError, setPromptsError] = useState('')
const [promptEditorOpen, setPromptEditorOpen] = useState(false)
const [editingDomainPrompt, setEditingDomainPrompt] = useState<DomainAiPrompt | null>(null)
const [promptDraftTemplateId, setPromptDraftTemplateId] = useState('')
const [promptDraftName, setPromptDraftName] = useState('')
const [promptDraftBody, setPromptDraftBody] = useState('')
const [promptEditorSubmitting, setPromptEditorSubmitting] = useState(false)
const [promptEditorError, setPromptEditorError] = useState('')
const [promptBusyId, setPromptBusyId] = useState<string | null>(null)
const [createOpen, setCreateOpen] = useState(false)
const [createName, setCreateName] = useState('')
const [createNameFa, setCreateNameFa] = useState('')
@@ -471,6 +496,137 @@ export function BusinessesPage() {
setEditOpen(true)
}
async function openAiPrompts(b: BusinessListItem) {
setPromptsBusiness(b)
setPromptsOpen(true)
setPromptsError('')
setDomainPrompts([])
setAllPromptTemplates([])
setPromptEditorOpen(false)
setEditingDomainPrompt(null)
if (!b.domainId) {
setPromptsError('Add a website/domain for this business before assigning AI prompts.')
return
}
setPromptsLoading(true)
try {
const [allTemplates, assigned] = await Promise.all([
listAiPromptTemplates(),
listDomainAiPrompts(b.domainId),
])
setAllPromptTemplates(allTemplates.items)
setDomainPrompts(assigned.items)
} catch (err) {
setPromptsError(
err instanceof ApiError ? err.message : 'Unable to load AI prompts.',
)
} finally {
setPromptsLoading(false)
}
}
function openAddDomainPrompt() {
setEditingDomainPrompt(null)
setPromptDraftTemplateId('')
setPromptDraftName('')
setPromptDraftBody('')
setPromptEditorError('')
setPromptEditorOpen(true)
}
function openEditDomainPrompt(prompt: DomainAiPrompt) {
setEditingDomainPrompt(prompt)
setPromptDraftTemplateId(prompt.sourceTemplateId ?? '')
setPromptDraftName(prompt.name)
setPromptDraftBody(prompt.body)
setPromptEditorError('')
setPromptEditorOpen(true)
}
function applyTemplateToDraft(templateId: string) {
setPromptDraftTemplateId(templateId)
const template = allPromptTemplates.find((item) => item.id === templateId)
if (!template) return
setPromptDraftName(template.name)
setPromptDraftBody(template.body)
}
async function submitDomainPromptEditor() {
if (!promptsBusiness?.domainId) return
const name = promptDraftName.trim()
const body = promptDraftBody.trim()
if (!name) {
setPromptEditorError('Name is required.')
return
}
if (!body) {
setPromptEditorError('Prompt text is required.')
return
}
setPromptEditorSubmitting(true)
setPromptEditorError('')
try {
if (editingDomainPrompt) {
const updated = await updateDomainAiPrompt(
promptsBusiness.domainId,
editingDomainPrompt.id,
{ name, body },
)
setDomainPrompts((prev) =>
prev.map((item) => (item.id === updated.id ? updated : item)),
)
showToast('Prompt updated.', 'success')
} else {
const created = await createDomainAiPrompt(promptsBusiness.domainId, {
name,
body,
...(promptDraftTemplateId
? { sourceTemplateId: Number(promptDraftTemplateId) }
: {}),
})
setDomainPrompts((prev) => [...prev, created])
showToast('Prompt added.', 'success')
}
setPromptEditorOpen(false)
setEditingDomainPrompt(null)
} catch (err) {
setPromptEditorError(
err instanceof ApiError ? err.message : 'Unable to save prompt.',
)
} finally {
setPromptEditorSubmitting(false)
}
}
async function removeDomainPromptItem(prompt: DomainAiPrompt) {
if (!promptsBusiness?.domainId) return
setPromptBusyId(prompt.id)
try {
await deleteDomainAiPrompt(promptsBusiness.domainId, prompt.id)
setDomainPrompts((prev) => prev.filter((item) => item.id !== prompt.id))
showToast('Prompt removed.', 'success')
} catch (err) {
showToast(
err instanceof ApiError ? err.message : 'Unable to remove prompt.',
'error',
)
} finally {
setPromptBusyId(null)
}
}
async function copyDomainPromptLink(url: string) {
try {
await navigator.clipboard.writeText(url)
showToast('Public link copied.', 'success')
} catch {
showToast('Unable to copy link.', 'error')
}
}
function goToBusinessUsers(b: BusinessListItem, membership: 'staff' | 'all') {
const params = new URLSearchParams({
businessId: String(b.id),
@@ -1460,6 +1616,7 @@ export function BusinessesPage() {
<BusinessActionsOverflowMenu
businessName={b.name}
host={b.domain}
onAiPrompts={() => void openAiPrompts(b)}
onAllUsers={() => goToBusinessUsers(b, 'all')}
onWipeData={() => openPurge(b)}
onRemove={() => setRemoveTarget(b)}
@@ -1498,6 +1655,187 @@ export function BusinessesPage() {
<Plus size={24} />
</button>
<Modal
open={promptsOpen}
title={
promptsBusiness
? `AI prompts · ${promptsBusiness.name}`
: 'AI prompts'
}
wide
onClose={() => {
if (promptEditorSubmitting) return
setPromptsOpen(false)
setPromptsBusiness(null)
setPromptsError('')
setPromptEditorOpen(false)
}}
>
{promptsError ? (
<p className={styles.alertError} role="alert">
{promptsError}
</p>
) : null}
{promptsLoading ? (
<p className={styles.subText}>Loading…</p>
) : promptsBusiness?.domainId ? (
<>
<div className={styles.promptsListHeader}>
<span className={styles.promptsListTitle}>
{domainPrompts.length} prompt{domainPrompts.length === 1 ? '' : 's'}
</span>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={openAddDomainPrompt}
disabled={promptEditorSubmitting}
>
<Plus size={16} />
Add
</button>
</div>
{domainPrompts.length === 0 ? (
<div className={styles.promptsEmpty}>
<FileText size={20} aria-hidden="true" />
<p>No prompts yet. Use Add to create one from a template.</p>
</div>
) : (
<ul className={styles.promptsList}>
{domainPrompts.map((prompt) => (
<li key={prompt.id} className={styles.promptsRow}>
<div className={styles.promptsRowText}>
<span className={styles.promptsRowName}>{prompt.name}</span>
<span className={styles.promptsRowPreview}>{prompt.body}</span>
</div>
<div className={styles.promptsRowActions}>
<button
type="button"
className={styles.controlBtn}
onClick={() => openEditDomainPrompt(prompt)}
disabled={promptBusyId === prompt.id}
aria-label={`Edit ${prompt.name}`}
title="Edit"
>
<Pencil size={15} />
</button>
<button
type="button"
className={styles.controlBtn}
onClick={() => void copyDomainPromptLink(prompt.publicUrl)}
disabled={promptBusyId === prompt.id}
aria-label={`Copy link for ${prompt.name}`}
title="Copy public link"
>
<Copy size={15} />
</button>
<button
type="button"
className={styles.controlBtn}
onClick={() => void removeDomainPromptItem(prompt)}
disabled={promptBusyId === prompt.id}
aria-label={`Remove ${prompt.name}`}
title="Remove"
>
<Trash2 size={15} />
</button>
</div>
</li>
))}
</ul>
)}
</>
) : null}
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => setPromptsOpen(false)}
disabled={promptEditorSubmitting}
>
Close
</button>
</div>
</Modal>
<Modal
open={promptEditorOpen}
title={editingDomainPrompt ? 'Edit prompt' : 'Add prompt'}
wide
onClose={() => {
if (promptEditorSubmitting) return
setPromptEditorOpen(false)
setEditingDomainPrompt(null)
setPromptEditorError('')
}}
>
{promptEditorError ? (
<p className={styles.alertError} role="alert">
{promptEditorError}
</p>
) : null}
<div className={styles.field}>
<label htmlFor="domain-prompt-template">Template</label>
<select
id="domain-prompt-template"
value={promptDraftTemplateId}
onChange={(e) => applyTemplateToDraft(e.target.value)}
disabled={promptEditorSubmitting || allPromptTemplates.length === 0}
>
<option value="">Select a template (optional)</option>
{allPromptTemplates.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</div>
<div className={styles.field}>
<label htmlFor="domain-prompt-name">Name</label>
<input
id="domain-prompt-name"
value={promptDraftName}
onChange={(e) => setPromptDraftName(e.target.value)}
placeholder="Prompt name"
disabled={promptEditorSubmitting}
/>
</div>
<div className={styles.field}>
<label htmlFor="domain-prompt-body">Prompt</label>
<textarea
id="domain-prompt-body"
className={styles.promptsTextarea}
value={promptDraftBody}
onChange={(e) => setPromptDraftBody(e.target.value)}
placeholder="Prompt text…"
disabled={promptEditorSubmitting}
rows={12}
/>
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => setPromptEditorOpen(false)}
disabled={promptEditorSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitDomainPromptEditor()}
disabled={promptEditorSubmitting}
>
{editingDomainPrompt ? 'Save' : 'Add'}
</button>
</div>
</Modal>
<Modal
open={editOpen}
title="Edit business"