feat(onboarding/phase-4): telephony, AI, widget config CRUD pages

Replaces the three remaining Pattern B placeholder routes
(/settings/telephony, /settings/ai, /settings/widget) with real forms
backed by the sidecar config endpoints introduced in Phase 1. Each page
loads the current config on mount, round-trips edits via PUT, and
supports reset-to-defaults. Changes take effect immediately since the
TelephonyConfigService / AiConfigService / WidgetConfigService all keep
in-memory caches that all consumers read through.

- src/components/forms/telephony-form.tsx — Ozonetel + SIP + Exotel
  sections; honours the '***masked***' sentinel for secrets
- src/components/forms/ai-form.tsx — provider/model/temperature/prompt
  with per-provider model suggestions
- src/components/forms/widget-form.tsx — enabled/url/embed toggles plus
  an allowedOrigins chip list
- src/pages/telephony-settings.tsx — loads masked config, marks the
  telephony wizard step complete when all required Ozonetel fields
  are filled
- src/pages/ai-settings.tsx — clamps temperature to 0..2 on save,
  marks the ai wizard step complete on successful save
- src/pages/widget-settings.tsx — uses the admin endpoint
  (/api/config/widget/admin), exposes rotate-key + copy-to-clipboard
  for the site key, and separates the read-only key card from the
  editable config card
- src/main.tsx — swaps the three placeholder routes for the real pages
- src/pages/settings-placeholder.tsx — removed; no longer referenced

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 07:49:32 +05:30
parent 4420b648d4
commit a7b2fd7fbe
8 changed files with 1006 additions and 78 deletions

View File

@@ -0,0 +1,136 @@
import { Input } from '@/components/base/input/input';
import { Select } from '@/components/base/select/select';
import { TextArea } from '@/components/base/textarea/textarea';
// AI assistant form — mirrors AiConfig in
// helix-engage-server/src/config/ai.defaults.ts. API keys stay in env vars
// (true secrets, rotated at the infra level); everything the admin can safely
// adjust lives here: provider choice, model, temperature, and an optional
// system-prompt addendum appended to the hospital-specific prompts that the
// WidgetChatService generates.
export type AiProvider = 'openai' | 'anthropic';
export type AiFormValues = {
provider: AiProvider;
model: string;
temperature: string;
systemPromptAddendum: string;
};
export const emptyAiFormValues = (): AiFormValues => ({
provider: 'openai',
model: 'gpt-4o-mini',
temperature: '0.7',
systemPromptAddendum: '',
});
const PROVIDER_ITEMS = [
{ id: 'openai', label: 'OpenAI' },
{ id: 'anthropic', label: 'Anthropic' },
];
// Recommended model presets per provider. Admin can still type any model
// string they want — these are suggestions, not the only options.
export const MODEL_SUGGESTIONS: Record<AiProvider, string[]> = {
openai: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
anthropic: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest', 'claude-3-opus-latest'],
};
type AiFormProps = {
value: AiFormValues;
onChange: (value: AiFormValues) => void;
};
export const AiForm = ({ value, onChange }: AiFormProps) => {
const patch = (updates: Partial<AiFormValues>) => onChange({ ...value, ...updates });
const suggestions = MODEL_SUGGESTIONS[value.provider];
return (
<div className="flex flex-col gap-6">
<section className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-primary">Provider & model</h3>
<p className="mt-1 text-xs text-tertiary">
Choose the AI vendor powering the website widget chat and call-summary features.
Changing providers takes effect immediately.
</p>
</div>
<Select
label="Provider"
placeholder="Select provider"
items={PROVIDER_ITEMS}
selectedKey={value.provider}
onSelectionChange={(key) => {
const next = key as AiProvider;
// When switching providers, also reset the model to the first
// suggested model for that provider — saves the admin a second
// edit step and avoids leaving an OpenAI model selected while
// provider=anthropic.
patch({
provider: next,
model: MODEL_SUGGESTIONS[next][0],
});
}}
>
{(item) => <Select.Item id={item.id} label={item.label} />}
</Select>
<div>
<Input
label="Model"
placeholder="Model identifier"
value={value.model}
onChange={(v) => patch({ model: v })}
/>
<div className="mt-2 flex flex-wrap gap-1.5">
{suggestions.map((model) => (
<button
key={model}
type="button"
onClick={() => patch({ model })}
className={`rounded-md border px-2 py-1 text-xs transition duration-100 ease-linear ${
value.model === model
? 'border-brand bg-brand-secondary text-brand-secondary'
: 'border-secondary bg-primary text-tertiary hover:bg-secondary'
}`}
>
{model}
</button>
))}
</div>
</div>
<Input
label="Temperature"
type="number"
placeholder="0.7"
hint="0 = deterministic, 1 = balanced, 2 = very creative"
value={value.temperature}
onChange={(v) => patch({ temperature: v })}
/>
</section>
<section className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-primary">System prompt addendum</h3>
<p className="mt-1 text-xs text-tertiary">
Optional gets appended to the hospital-specific prompts the widget generates
automatically from your doctors and clinics. Use this to add tone guidelines,
escalation rules, or topics the assistant should avoid. Leave blank for the default
behaviour.
</p>
</div>
<TextArea
label="Additional instructions"
placeholder="e.g. Always respond in the patient's language. Never quote specific medication dosages; refer them to a doctor for prescriptions."
value={value.systemPromptAddendum}
onChange={(v) => patch({ systemPromptAddendum: v })}
rows={6}
/>
</section>
</div>
);
};