PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 All 85 releases
yatra / assets / dist / js / AiAssistant-Bu_zHUnt.js

AiAssistant-Bu_zHUnt.js in Yatra – Travel Booking & Tour Operator Software 3.0.17, at assets/dist/js/AiAssistant-Bu_zHUnt.js

1,236 lines 66.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { v as useQueryClient, r as reactExports, u as useQuery, w as useMutation, j as jsxRuntimeExports, bW as KeyRound, S as Sparkles, bF as Pencil, m as BarChart3, X as ExternalLink, aG as Info, bU as Plug, aE as CheckCircle2, b9 as XCircle, by as EyeOff, aR as Eye, aW as Save, E as Loader2, aO as Trash2, bb as Lock, M as MessageCircle, R as RotateCcw, V as AlertTriangle, bd as ChevronUp, y as ChevronDown, C as Crown } from "./react-vendor-CithbthY.js";
2 import { u as useToast, _ as __, I as Input } from "./index-Dec_weLB.js";
3 import { P as PageHeader, C as Card, d as CardContent, f as CardHeader, g as CardTitle, h as CardDescription, B as Button, s as Label, S as Select, c as aiApi } from "../../admin/dist/js/app.js";
4 import { M as ModulePageSkeleton, d as ModuleSectionSkeleton, e as ModuleStatGridSkeleton } from "./module-skeleton-CpoAoviJ.js";
5 const SECTIONS = [
6 {
7 id: "keys",
8 label: "API Keys",
9 icon: KeyRound,
10 description: "Bring your own OpenAI or Anthropic key. Keys are stored encrypted and never leave your server."
11 },
12 {
13 id: "voice",
14 label: "Brand Voice",
15 icon: Sparkles,
16 description: "Tone, examples, vocabulary preferences — applied to every generation so AI output sounds like your business."
17 },
18 {
19 id: "prompts",
20 label: "Prompts",
21 icon: Pencil,
22 description: "Inspect or edit every system + user prompt the assistant uses. Overrides fall back to defaults per-field."
23 },
24 {
25 id: "usage",
26 label: "Usage",
27 icon: BarChart3,
28 description: "Token usage by month, per provider, per feature."
29 }
30 ];
31 const DEFAULT_BRAND_VOICE = {
32 tone: "",
33 examples: [],
34 forbidden: [],
35 required: [],
36 language: "en-US",
37 default_provider: "openai",
38 default_model: ""
39 };
40 const PlanBadge = ({ tier }) => {
41 if (tier === "agency") {
42 return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-gradient-to-r from-purple-600 to-indigo-500 px-2.5 py-1 text-xs font-medium text-white shadow-sm", children: [
43 /* @__PURE__ */ jsxRuntimeExports.jsx(Crown, { className: "h-3 w-3" }),
44 __("Scale plan", "yatra")
45 ] });
46 }
47 return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-gradient-to-r from-emerald-500 to-teal-500 px-2.5 py-1 text-xs font-medium text-white shadow-sm", children: [
48 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-3 w-3" }),
49 __("Growth plan", "yatra")
50 ] });
51 };
52 const UpgradeCard = ({ meta }) => {
53 const upgradeUrl = (meta == null ? void 0 : meta.upgrade_url) || "https://wpyatra.com/pricing?module=ai-assistant";
54 const licensePageUrl = (meta == null ? void 0 : meta.license_page_url) || "admin.php?page=yatra&subpage=license";
55 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { className: "max-w-3xl", children: [
56 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [
57 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md bg-blue-100 p-2 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "h-5 w-5" }) }),
58 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
59 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
60 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("AI Assistant", "yatra") }),
61 /* @__PURE__ */ jsxRuntimeExports.jsx(PlanBadge, { tier: "growth" })
62 ] }),
63 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { className: "mt-1", children: __(
64 "AI Assistant unlocks on the Growth plan (or Scale). Bring your own OpenAI or Anthropic key — costs are paid directly to your provider, no per-call markup from the plugin.",
65 "yatra"
66 ) })
67 ] })
68 ] }) }),
69 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
70 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: __(
71 "Inline AI affordances on trip descriptions, SEO meta, included/excluded items, FAQs, and more — write less, finish more.",
72 "yatra"
73 ) }),
74 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-3 pt-2", children: [
75 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: upgradeUrl, target: "_blank", rel: "noopener noreferrer", children: [
76 (meta == null ? void 0 : meta.is_pro_active) ? __("Upgrade plan", "yatra") : __("Get Yatra Pro Growth", "yatra"),
77 /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "ml-2 h-4 w-4" })
78 ] }) }),
79 (meta == null ? void 0 : meta.is_pro_active) && /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: licensePageUrl, children: __("Manage License", "yatra") }) })
80 ] })
81 ] })
82 ] });
83 };
84 const AiAssistant = () => {
85 const { showToast } = useToast();
86 const queryClient = useQueryClient();
87 const [activeSection, setActiveSection] = reactExports.useState("keys");
88 const { data: meta, isLoading: metaLoading } = useQuery({
89 queryKey: ["ai-meta"],
90 queryFn: () => aiApi.meta()
91 });
92 const { data: bvResp } = useQuery({
93 queryKey: ["ai-brand-voice"],
94 queryFn: () => aiApi.getBrandVoice(),
95 enabled: Boolean(meta == null ? void 0 : meta.is_ai_eligible)
96 });
97 const [bv, setBv] = reactExports.useState(DEFAULT_BRAND_VOICE);
98 reactExports.useEffect(() => {
99 if (bvResp == null ? void 0 : bvResp.data) {
100 setBv({ ...DEFAULT_BRAND_VOICE, ...bvResp.data });
101 }
102 }, [bvResp]);
103 const saveBvMutation = useMutation({
104 mutationFn: (data) => aiApi.saveBrandVoice(data),
105 onSuccess: () => {
106 showToast(__("Brand voice saved.", "yatra"), "success");
107 queryClient.invalidateQueries({ queryKey: ["ai-brand-voice"] });
108 },
109 onError: (e) => showToast(extractError(e), "error")
110 });
111 const [keyDrafts, setKeyDrafts] = reactExports.useState({});
112 const [showKey, setShowKey] = reactExports.useState({});
113 const setKeyMutation = useMutation({
114 mutationFn: ({ provider, key }) => aiApi.setKey(provider, key),
115 onSuccess: (_resp, vars) => {
116 showToast(__("API key saved.", "yatra"), "success");
117 setKeyDrafts((d) => ({ ...d, [vars.provider]: "" }));
118 queryClient.invalidateQueries({ queryKey: ["ai-meta"] });
119 },
120 onError: (e) => showToast(extractError(e), "error")
121 });
122 const deleteKeyMutation = useMutation({
123 mutationFn: (provider) => aiApi.deleteKey(provider),
124 onSuccess: () => {
125 showToast(__("API key cleared.", "yatra"), "success");
126 queryClient.invalidateQueries({ queryKey: ["ai-meta"] });
127 },
128 onError: (e) => showToast(extractError(e), "error")
129 });
130 const testKeyMutation = useMutation({
131 mutationFn: (provider) => aiApi.testKey(provider),
132 onSuccess: (resp) => {
133 showToast(
134 resp.message || __("Connection successful.", "yatra"),
135 "success"
136 );
137 },
138 onError: (e) => showToast(extractError(e), "error")
139 });
140 const tripChatMutation = useMutation({
141 mutationFn: (enabled) => aiApi.setTripChatEnabled(enabled),
142 onSuccess: (resp) => {
143 showToast(resp.message, "success");
144 queryClient.invalidateQueries({ queryKey: ["ai-meta"] });
145 },
146 onError: (e) => showToast(extractError(e), "error")
147 });
148 const [chatLimitsDraft, setChatLimitsDraft] = reactExports.useState({
149 per_trip_day: "",
150 per_ip_hour: "",
151 per_session: "",
152 per_session_booking: "",
153 max_message_chars: "",
154 history_turns: ""
155 });
156 reactExports.useEffect(() => {
157 if (meta == null ? void 0 : meta.trip_chat_limits) {
158 const l = meta.trip_chat_limits;
159 setChatLimitsDraft({
160 per_trip_day: String(l.per_trip_day),
161 per_ip_hour: String(l.per_ip_hour),
162 per_session: String(l.per_session),
163 per_session_booking: String(l.per_session_booking),
164 max_message_chars: String(l.max_message_chars),
165 history_turns: String(l.history_turns)
166 });
167 }
168 }, [meta == null ? void 0 : meta.trip_chat_limits]);
169 const tripChatLimitsMutation = useMutation({
170 mutationFn: (patch) => aiApi.setTripChatLimits(patch),
171 onSuccess: (resp) => {
172 showToast(resp.message, "success");
173 queryClient.invalidateQueries({ queryKey: ["ai-meta"] });
174 },
175 onError: (e) => showToast(extractError(e), "error")
176 });
177 const { data: usageResp } = useQuery({
178 queryKey: ["ai-usage"],
179 queryFn: () => aiApi.getUsage(),
180 enabled: Boolean(meta == null ? void 0 : meta.is_ai_eligible) && activeSection === "usage"
181 });
182 const { data: promptsResp, isLoading: promptsLoading } = useQuery({
183 queryKey: ["ai-prompts"],
184 queryFn: () => aiApi.listPrompts(),
185 enabled: Boolean(meta == null ? void 0 : meta.is_ai_eligible) && activeSection === "prompts"
186 });
187 const [expandedPrompt, setExpandedPrompt] = reactExports.useState(null);
188 const [promptDrafts, setPromptDrafts] = reactExports.useState({});
189 reactExports.useEffect(() => {
190 if (!(promptsResp == null ? void 0 : promptsResp.data)) return;
191 const next = {};
192 for (const row of promptsResp.data) {
193 const effSystem = row.override.system && row.override.system !== "" ? row.override.system : row.default.system;
194 const effUser = row.override.user && row.override.user !== "" ? row.override.user : row.default.user;
195 const effMaxTokens = row.override.max_tokens != null ? String(row.override.max_tokens) : String(row.default.max_tokens);
196 const effTemperature = row.override.temperature != null ? String(row.override.temperature) : String(row.default.temperature);
197 next[row.task] = {
198 system: effSystem,
199 user: effUser,
200 max_tokens: effMaxTokens,
201 temperature: effTemperature
202 };
203 }
204 setPromptDrafts(next);
205 }, [promptsResp]);
206 const savePromptMutation = useMutation({
207 mutationFn: ({
208 task,
209 override
210 }) => aiApi.savePromptOverride(task, override),
211 onSuccess: () => {
212 showToast(__("Prompt saved.", "yatra"), "success");
213 queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
214 },
215 onError: (e) => showToast(extractError(e), "error")
216 });
217 const resetPromptMutation = useMutation({
218 mutationFn: (task) => aiApi.resetPrompt(task),
219 onSuccess: () => {
220 showToast(__("Prompt reset to default.", "yatra"), "success");
221 queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
222 },
223 onError: (e) => showToast(extractError(e), "error")
224 });
225 const resetAllPromptsMutation = useMutation({
226 mutationFn: () => aiApi.resetAllPrompts(),
227 onSuccess: () => {
228 showToast(__("All prompts reset to defaults.", "yatra"), "success");
229 queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
230 },
231 onError: (e) => showToast(extractError(e), "error")
232 });
233 if (metaLoading) {
234 return /* @__PURE__ */ jsxRuntimeExports.jsx(ModulePageSkeleton, { variant: "tabs" });
235 }
236 if (!meta || !meta.is_ai_eligible) {
237 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
238 /* @__PURE__ */ jsxRuntimeExports.jsx(
239 PageHeader,
240 {
241 description: __(
242 "Inline AI generation for trip content, SEO, and more.",
243 "yatra"
244 )
245 }
246 ),
247 /* @__PURE__ */ jsxRuntimeExports.jsx(UpgradeCard, { meta })
248 ] });
249 }
250 const moduleEnabled = meta.is_module_enabled;
251 const updateBv = (key, value) => setBv((prev) => ({ ...prev, [key]: value }));
252 const updateBvList = (key, raw) => {
253 const list = raw.split(/\r?\n/).map((s) => s.trim()).filter((s) => s !== "");
254 setBv((prev) => ({ ...prev, [key]: list }));
255 };
256 const submitBv = (e) => {
257 e.preventDefault();
258 saveBvMutation.mutate(bv);
259 };
260 const providerDocs = {
261 openai: {
262 whyDescription: __(
263 "Authenticates every request the plugin sends to OpenAI on your behalf. OpenAI bills your account directly per request — the plugin adds no markup. Without a key, the AI sparkle affordances on Trip / SEO / Email editors stay disabled.",
264 "yatra"
265 ),
266 whereToFind: __(
267 "Sign in at platform.openai.com → API keys → Create new secret key. Copy the value (starts with sk-…) immediately — OpenAI shows it only once.",
268 "yatra"
269 ),
270 docsUrl: "https://platform.openai.com/api-keys",
271 pricingUrl: "https://openai.com/api/pricing/"
272 },
273 anthropic: {
274 whyDescription: __(
275 "Authenticates requests to Anthropic's Claude API. Anthropic bills your account directly per request — no plugin markup. Used for the same in-editor sparkle features OpenAI handles; pick whichever provider your team prefers.",
276 "yatra"
277 ),
278 whereToFind: __(
279 "Sign in at console.anthropic.com → Settings → API Keys → Create Key. Copy the value (starts with sk-ant-…) immediately — Anthropic shows it only once.",
280 "yatra"
281 ),
282 docsUrl: "https://console.anthropic.com/settings/keys",
283 pricingUrl: "https://www.anthropic.com/pricing#anthropic-api"
284 }
285 };
286 const renderKeys = () => /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
287 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
288 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("API Keys", "yatra") }),
289 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
290 "Keys are encrypted at rest with libsodium and never sent to the browser. Generation requests are made from your server directly to your chosen provider — no third-party proxy is involved.",
291 "yatra"
292 ) })
293 ] }),
294 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
295 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md border border-blue-200 bg-blue-50/40 p-3 text-sm dark:border-blue-700 dark:bg-blue-900/20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2", children: [
296 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-blue-600 dark:text-blue-400" }),
297 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2 text-blue-900 dark:text-blue-200", children: [
298 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium", children: __("How AI Assistant billing works", "yatra") }),
299 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs", children: __(
300 "You bring your own OpenAI or Anthropic key. Each AI generation (trip description, FAQ, email body, etc.) is a single API call your provider charges you for — usually fractions of a cent. The plugin never proxies traffic and adds no markup. You only need one key to start; you can save both and pick a default per task under the Defaults tab.",
301 "yatra"
302 ) }),
303 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] opacity-80", children: __(
304 'Click "Where to find this" next to a provider for the exact steps in their dashboard.',
305 "yatra"
306 ) })
307 ] })
308 ] }) }),
309 meta.providers.map((p) => {
310 const status = meta.keys[p.id] ?? { configured: false, hint: "" };
311 const draft = keyDrafts[p.id] ?? "";
312 const visible = Boolean(showKey[p.id]);
313 const docs = providerDocs[p.id];
314 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
315 "div",
316 {
317 className: "rounded-md border border-gray-200 p-3 dark:border-gray-700",
318 children: [
319 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
320 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
321 /* @__PURE__ */ jsxRuntimeExports.jsx(Plug, { className: "h-4 w-4 text-gray-500" }),
322 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium text-gray-900 dark:text-white", children: p.label }),
323 status.configured ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text-[11px] font-medium text-green-700 dark:bg-green-900/30 dark:text-green-300", children: [
324 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-3 w-3" }),
325 __("Configured", "yatra")
326 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: [
327 /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "h-3 w-3" }),
328 __("Not configured", "yatra")
329 ] })
330 ] }),
331 status.configured && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500", children: status.hint })
332 ] }),
333 docs && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3 space-y-2 rounded-md border border-gray-100 bg-gray-50/60 p-3 text-xs text-gray-700 dark:border-gray-700 dark:bg-gray-800/40 dark:text-gray-300", children: [
334 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-1.5", children: [
335 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-500" }),
336 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
337 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-gray-900 dark:text-white", children: __("Why it's needed:", "yatra") }),
338 " ",
339 docs.whyDescription
340 ] })
341 ] }),
342 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-1.5", children: [
343 /* @__PURE__ */ jsxRuntimeExports.jsx(KeyRound, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-500" }),
344 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
345 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-gray-900 dark:text-white", children: __("Where to find it:", "yatra") }),
346 " ",
347 docs.whereToFind
348 ] })
349 ] }),
350 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-2 pt-1", children: [
351 /* @__PURE__ */ jsxRuntimeExports.jsxs(
352 "a",
353 {
354 href: docs.docsUrl,
355 target: "_blank",
356 rel: "noopener noreferrer",
357 className: "inline-flex items-center gap-1 rounded border border-gray-300 px-2 py-0.5 text-[11px] text-gray-700 hover:bg-white dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800",
358 children: [
359 __("Open API keys page", "yatra"),
360 /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "h-3 w-3" })
361 ]
362 }
363 ),
364 /* @__PURE__ */ jsxRuntimeExports.jsxs(
365 "a",
366 {
367 href: docs.pricingUrl,
368 target: "_blank",
369 rel: "noopener noreferrer",
370 className: "inline-flex items-center gap-1 rounded border border-gray-300 px-2 py-0.5 text-[11px] text-gray-700 hover:bg-white dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800",
371 children: [
372 __("Pricing", "yatra"),
373 /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "h-3 w-3" })
374 ]
375 }
376 )
377 ] })
378 ] }),
379 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [
380 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-[240px]", children: [
381 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: `ai-key-${p.id}`, children: __("API key", "yatra") }),
382 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative mt-1", children: [
383 /* @__PURE__ */ jsxRuntimeExports.jsx(
384 Input,
385 {
386 id: `ai-key-${p.id}`,
387 type: visible ? "text" : "password",
388 value: draft,
389 placeholder: status.configured ? __("Enter a new key to replace…", "yatra") : __("sk-… or sk-ant-…", "yatra"),
390 autoComplete: "off",
391 onChange: (e) => setKeyDrafts((d) => ({
392 ...d,
393 [p.id]: e.target.value
394 }))
395 }
396 ),
397 /* @__PURE__ */ jsxRuntimeExports.jsx(
398 "button",
399 {
400 type: "button",
401 onClick: () => setShowKey((s) => ({ ...s, [p.id]: !s[p.id] })),
402 className: "absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600",
403 "aria-label": visible ? __("Hide key", "yatra") : __("Show key", "yatra"),
404 children: visible ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "h-4 w-4" })
405 }
406 )
407 ] })
408 ] }),
409 /* @__PURE__ */ jsxRuntimeExports.jsxs(
410 Button,
411 {
412 type: "button",
413 onClick: () => setKeyMutation.mutate({ provider: p.id, key: draft }),
414 disabled: draft.trim() === "" || setKeyMutation.isPending,
415 children: [
416 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-2 h-4 w-4" }),
417 __("Save", "yatra")
418 ]
419 }
420 ),
421 status.configured && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
422 /* @__PURE__ */ jsxRuntimeExports.jsxs(
423 Button,
424 {
425 type: "button",
426 variant: "outline",
427 onClick: () => testKeyMutation.mutate(p.id),
428 disabled: testKeyMutation.isPending,
429 children: [
430 testKeyMutation.isPending && testKeyMutation.variables === p.id ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null,
431 __("Test connection", "yatra")
432 ]
433 }
434 ),
435 /* @__PURE__ */ jsxRuntimeExports.jsxs(
436 Button,
437 {
438 type: "button",
439 variant: "ghost",
440 onClick: () => deleteKeyMutation.mutate(p.id),
441 disabled: deleteKeyMutation.isPending,
442 children: [
443 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "mr-2 h-4 w-4" }),
444 __("Clear", "yatra")
445 ]
446 }
447 )
448 ] })
449 ] })
450 ]
451 },
452 p.id
453 );
454 }),
455 !moduleEnabled && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50/40 p-3 text-xs text-amber-800 dark:border-amber-500/40 dark:bg-amber-900/20 dark:text-amber-200", children: [
456 /* @__PURE__ */ jsxRuntimeExports.jsx(Lock, { className: "mt-0.5 h-4 w-4 shrink-0" }),
457 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
458 __(
459 "AI Assistant module is not enabled yet. Once a key is saved, enable the module from the Modules page so sparkle affordances appear in the editors.",
460 "yatra"
461 ),
462 " ",
463 /* @__PURE__ */ jsxRuntimeExports.jsx(
464 "a",
465 {
466 className: "font-medium underline",
467 href: "admin.php?page=yatra&subpage=modules",
468 children: __("Go to Modules →", "yatra")
469 }
470 )
471 ] })
472 ] }),
473 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 p-3 dark:border-gray-700", children: [
474 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
475 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2", children: [
476 /* @__PURE__ */ jsxRuntimeExports.jsx(MessageCircle, { className: "mt-0.5 h-4 w-4 text-blue-600 dark:text-blue-300" }),
477 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
478 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white", children: __("Public Chat Widget", "yatra") }),
479 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-0.5 text-xs text-gray-500 dark:text-gray-400", children: __(
480 `Adds a "Chat with AI about this trip" button below the Send Enquiry button on every single-trip page. Visitors can ask trip-specific questions. Rate-limited per IP/session/trip/day so a hostile visitor can't burn your provider budget.`,
481 "yatra"
482 ) })
483 ] })
484 ] }),
485 /* @__PURE__ */ jsxRuntimeExports.jsx(
486 "button",
487 {
488 type: "button",
489 role: "switch",
490 "aria-checked": Boolean(meta.trip_chat_enabled),
491 onClick: () => tripChatMutation.mutate(!meta.trip_chat_enabled),
492 disabled: tripChatMutation.isPending || !moduleEnabled,
493 className: `relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors ${meta.trip_chat_enabled ? "bg-blue-600" : "bg-gray-300 dark:bg-gray-600"} ${tripChatMutation.isPending || !moduleEnabled ? "opacity-50 cursor-not-allowed" : ""}`,
494 children: /* @__PURE__ */ jsxRuntimeExports.jsx(
495 "span",
496 {
497 className: `inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${meta.trip_chat_enabled ? "translate-x-5" : "translate-x-0.5"} translate-y-0.5`
498 }
499 )
500 }
501 )
502 ] }),
503 !moduleEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-[11px] text-amber-600 dark:text-amber-300", children: __(
504 "Enable the AI Assistant module first, then turn this on.",
505 "yatra"
506 ) }),
507 meta.trip_chat_enabled && meta.trip_chat_limits_schema && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 space-y-3 border-t border-gray-200 pt-3 dark:border-gray-700", children: [
508 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
509 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200", children: __("Chat rate limits", "yatra") }),
510 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-0.5 text-[11px] text-gray-500 dark:text-gray-400", children: __(
511 "Caps on the public chat widget to protect your AI provider budget. Higher numbers = more freedom for visitors and more potential cost. Each field is clamped to a safe range on save.",
512 "yatra"
513 ) })
514 ] }),
515 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: [
516 /* @__PURE__ */ jsxRuntimeExports.jsx(
517 LimitField,
518 {
519 label: __("Per trip · per day", "yatra"),
520 hint: __("Max chat messages per trip per day.", "yatra"),
521 field: "per_trip_day",
522 draft: chatLimitsDraft,
523 setDraft: setChatLimitsDraft,
524 schema: meta.trip_chat_limits_schema
525 }
526 ),
527 /* @__PURE__ */ jsxRuntimeExports.jsx(
528 LimitField,
529 {
530 label: __("Per IP · per hour", "yatra"),
531 hint: __(
532 "Max messages from one visitor IP per hour.",
533 "yatra"
534 ),
535 field: "per_ip_hour",
536 draft: chatLimitsDraft,
537 setDraft: setChatLimitsDraft,
538 schema: meta.trip_chat_limits_schema
539 }
540 ),
541 /* @__PURE__ */ jsxRuntimeExports.jsx(
542 LimitField,
543 {
544 label: __("Per session (info)", "yatra"),
545 hint: __(
546 "Cap when visitor is just asking questions.",
547 "yatra"
548 ),
549 field: "per_session",
550 draft: chatLimitsDraft,
551 setDraft: setChatLimitsDraft,
552 schema: meta.trip_chat_limits_schema
553 }
554 ),
555 /* @__PURE__ */ jsxRuntimeExports.jsx(
556 LimitField,
557 {
558 label: __("Per session (booking)", "yatra"),
559 hint: __(
560 "Higher cap once visitor signals booking intent.",
561 "yatra"
562 ),
563 field: "per_session_booking",
564 draft: chatLimitsDraft,
565 setDraft: setChatLimitsDraft,
566 schema: meta.trip_chat_limits_schema
567 }
568 ),
569 /* @__PURE__ */ jsxRuntimeExports.jsx(
570 LimitField,
571 {
572 label: __("Max message length", "yatra"),
573 hint: __(
574 "Max characters in a single visitor message.",
575 "yatra"
576 ),
577 field: "max_message_chars",
578 draft: chatLimitsDraft,
579 setDraft: setChatLimitsDraft,
580 schema: meta.trip_chat_limits_schema
581 }
582 ),
583 /* @__PURE__ */ jsxRuntimeExports.jsx(
584 LimitField,
585 {
586 label: __("History turns sent", "yatra"),
587 hint: __(
588 "How many recent turns the AI sees for context.",
589 "yatra"
590 ),
591 field: "history_turns",
592 draft: chatLimitsDraft,
593 setDraft: setChatLimitsDraft,
594 schema: meta.trip_chat_limits_schema
595 }
596 )
597 ] }),
598 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
599 /* @__PURE__ */ jsxRuntimeExports.jsx(
600 Button,
601 {
602 type: "button",
603 variant: "outline",
604 onClick: () => {
605 if (meta.trip_chat_limits) {
606 const l = meta.trip_chat_limits;
607 setChatLimitsDraft({
608 per_trip_day: String(l.per_trip_day),
609 per_ip_hour: String(l.per_ip_hour),
610 per_session: String(l.per_session),
611 per_session_booking: String(l.per_session_booking),
612 max_message_chars: String(l.max_message_chars),
613 history_turns: String(l.history_turns)
614 });
615 }
616 },
617 disabled: tripChatLimitsMutation.isPending,
618 children: __("Revert", "yatra")
619 }
620 ),
621 /* @__PURE__ */ jsxRuntimeExports.jsx(
622 Button,
623 {
624 type: "button",
625 onClick: () => {
626 const patch = {};
627 Object.keys(chatLimitsDraft).forEach((k) => {
628 const v = parseInt(chatLimitsDraft[k], 10);
629 if (Number.isFinite(v)) patch[k] = v;
630 });
631 tripChatLimitsMutation.mutate(patch);
632 },
633 disabled: tripChatLimitsMutation.isPending,
634 children: tripChatLimitsMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
635 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
636 __("Saving…", "yatra")
637 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
638 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-2 h-4 w-4" }),
639 __("Save limits", "yatra")
640 ] })
641 }
642 )
643 ] })
644 ] })
645 ] })
646 ] })
647 ] });
648 const renderVoice = () => {
649 var _a;
650 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
651 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
652 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Brand voice", "yatra") }),
653 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
654 "Define your voice once — it flows into every generation across trips, SEO, and emails. The more specific, the more your AI output sounds like you.",
655 "yatra"
656 ) })
657 ] }),
658 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { children: [
659 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 rounded-md border border-blue-200 bg-blue-50/60 p-3 text-xs text-blue-900 dark:border-blue-500/40 dark:bg-blue-900/20 dark:text-blue-200", children: [
660 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-1 font-semibold", children: __("How brand voice is used", "yatra") }),
661 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "leading-relaxed", children: __(
662 "Everything you fill in here is automatically prepended to every prompt the plugin sends to your AI provider. Trip descriptions, enquiry replies, email templates, the public chat widget, and the back-office wizards all start with these instructions — so your output sounds like one consistent brand, not a different voice per feature.",
663 "yatra"
664 ) })
665 ] }),
666 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: submitBv, className: "space-y-5", children: [
667 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
668 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-tone", children: __("Tone description", "yatra") }),
669 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
670 "The single most important field. 2-3 sentences describing how you'd describe your brand to a new copywriter. Plain English — not marketing jargon. AI reads this every generation and aims for this tone.",
671 "yatra"
672 ) }),
673 /* @__PURE__ */ jsxRuntimeExports.jsx(
674 "textarea",
675 {
676 id: "bv-tone",
677 rows: 3,
678 className: "mt-1 w-full rounded-md border border-gray-300 bg-white p-2 text-sm dark:border-gray-600 dark:bg-gray-800",
679 value: bv.tone,
680 placeholder: __(
681 "e.g. Warm, adventurous, never corporate. Speak directly to the traveler. We're a small team, not a corporation.",
682 "yatra"
683 ),
684 onChange: (e) => updateBv("tone", e.target.value)
685 }
686 )
687 ] }),
688 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
689 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-examples", children: __("Style examples (one paragraph per line)", "yatra") }),
690 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
691 "Paste 3-5 paragraphs from your existing trip pages, emails, or marketing copy that sound right. AI uses these as few-shot examples — concrete writing samples beat abstract tone descriptions for getting the voice exactly right. One paragraph per line.",
692 "yatra"
693 ) }),
694 /* @__PURE__ */ jsxRuntimeExports.jsx(
695 "textarea",
696 {
697 id: "bv-examples",
698 rows: 6,
699 className: "mt-1 w-full rounded-md border border-gray-300 bg-white p-2 font-mono text-xs dark:border-gray-600 dark:bg-gray-800",
700 value: bv.examples.join("\n"),
701 placeholder: __(
702 "Paste 3-5 paragraphs from your existing trips/emails that sound like you. AI uses these as few-shot examples.",
703 "yatra"
704 ),
705 onChange: (e) => updateBvList("examples", e.target.value)
706 }
707 )
708 ] }),
709 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
710 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
711 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-forbidden", children: __("Forbidden words / phrases", "yatra") }),
712 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
713 'Words AI must never use. Common entries: "unforgettable", "once in a lifetime", "breathtaking", "world-class", or any competitor name. One per line — AI treats these as a hard blocklist.',
714 "yatra"
715 ) }),
716 /* @__PURE__ */ jsxRuntimeExports.jsx(
717 "textarea",
718 {
719 id: "bv-forbidden",
720 rows: 4,
721 className: "mt-1 w-full rounded-md border border-gray-300 bg-white p-2 text-xs dark:border-gray-600 dark:bg-gray-800",
722 value: bv.forbidden.join("\n"),
723 placeholder: __(
724 "One per line — words / phrases to avoid.",
725 "yatra"
726 ),
727 onChange: (e) => updateBvList("forbidden", e.target.value)
728 }
729 )
730 ] }),
731 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
732 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-required", children: __("Required mentions", "yatra") }),
733 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
734 'Things AI should weave in WHEN RELEVANT — never forced. Examples: "local guides", "small group sizes", "sustainable practices", "our 24/7 support". One per line. AI mentions these only when they fit the context naturally.',
735 "yatra"
736 ) }),
737 /* @__PURE__ */ jsxRuntimeExports.jsx(
738 "textarea",
739 {
740 id: "bv-required",
741 rows: 4,
742 className: "mt-1 w-full rounded-md border border-gray-300 bg-white p-2 text-xs dark:border-gray-600 dark:bg-gray-800",
743 value: bv.required.join("\n"),
744 placeholder: __(
745 "One per line — things to weave in when relevant.",
746 "yatra"
747 ),
748 onChange: (e) => updateBvList("required", e.target.value)
749 }
750 )
751 ] })
752 ] }),
753 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [
754 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
755 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-lang", children: __("Default language", "yatra") }),
756 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
757 "BCP-47 code. AI writes in this language unless overridden by per-prompt context (e.g. enquiry replies match the customer's preferred language when known).",
758 "yatra"
759 ) }),
760 /* @__PURE__ */ jsxRuntimeExports.jsx(
761 Input,
762 {
763 id: "bv-lang",
764 value: bv.language,
765 placeholder: "en-US",
766 onChange: (e) => updateBv("language", e.target.value)
767 }
768 )
769 ] }),
770 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
771 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-provider", children: __("Default provider", "yatra") }),
772 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
773 "Which AI service handles each generation by default. Per-feature usage runs through the provider whose API key is configured under API Keys above.",
774 "yatra"
775 ) }),
776 /* @__PURE__ */ jsxRuntimeExports.jsx(
777 Select,
778 {
779 id: "bv-provider",
780 value: bv.default_provider,
781 onChange: (e) => updateBv("default_provider", e.target.value),
782 children: meta.providers.map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, children: p.label }, p.id))
783 }
784 )
785 ] }),
786 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
787 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "bv-model", children: __("Default model", "yatra") }),
788 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mb-1.5 text-xs text-gray-500 dark:text-gray-400", children: __(
789 "Specific model within the provider. Smaller / faster models (gpt-4o-mini, Claude Haiku) are cheaper and ~good enough for most generations. Larger models are slower + costlier but produce richer output for trip descriptions.",
790 "yatra"
791 ) }),
792 /* @__PURE__ */ jsxRuntimeExports.jsxs(
793 Select,
794 {
795 id: "bv-model",
796 value: bv.default_model,
797 onChange: (e) => updateBv("default_model", e.target.value),
798 children: [
799 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("(Provider default)", "yatra") }),
800 (((_a = meta.providers.find((p) => p.id === bv.default_provider)) == null ? void 0 : _a.models) ?? []).map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m.id, children: m.label }, m.id))
801 ]
802 }
803 )
804 ] })
805 ] }),
806 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3 pt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { type: "submit", disabled: saveBvMutation.isPending, children: saveBvMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
807 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
808 __("Saving…", "yatra")
809 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
810 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-2 h-4 w-4" }),
811 __("Save brand voice", "yatra")
812 ] }) }) })
813 ] })
814 ] })
815 ] });
816 };
817 const renderUsage = () => {
818 const usage = usageResp == null ? void 0 : usageResp.data;
819 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
820 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
821 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Usage", "yatra") }),
822 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
823 "Rolling 13-month token totals, per provider, per task. Costs are paid directly to your AI provider — no markup is added.",
824 "yatra"
825 ) })
826 ] }),
827 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { children: !usage ? /* @__PURE__ */ jsxRuntimeExports.jsx(ModuleStatGridSkeleton, { tiles: 3 }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
828 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 grid grid-cols-3 gap-3", children: [
829 /* @__PURE__ */ jsxRuntimeExports.jsx(
830 Stat,
831 {
832 label: __("Total calls", "yatra"),
833 value: usage.totals.calls.toLocaleString()
834 }
835 ),
836 /* @__PURE__ */ jsxRuntimeExports.jsx(
837 Stat,
838 {
839 label: __("Prompt tokens", "yatra"),
840 value: usage.totals.prompt.toLocaleString()
841 }
842 ),
843 /* @__PURE__ */ jsxRuntimeExports.jsx(
844 Stat,
845 {
846 label: __("Completion tokens", "yatra"),
847 value: usage.totals.completion.toLocaleString()
848 }
849 )
850 ] }),
851 Object.keys(usage.months).length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500", children: __("No AI usage yet.", "yatra") }),
852 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-3", children: Object.entries(usage.months).map(([month, providers]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
853 "div",
854 {
855 className: "rounded-md border border-gray-200 p-3 dark:border-gray-700",
856 children: [
857 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-2 text-sm font-medium text-gray-900 dark:text-white", children: [
858 month,
859 month === usage.current_month && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-2 rounded-full bg-blue-100 px-2 py-0.5 text-[10px] font-semibold uppercase text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", children: __("Current", "yatra") })
860 ] }),
861 Object.entries(providers).map(([prov, bucket]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
862 "div",
863 {
864 className: "mb-1 grid grid-cols-4 gap-2 text-xs",
865 children: [
866 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium text-gray-700 dark:text-gray-200", children: prov }),
867 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
868 bucket.calls,
869 " ",
870 __("calls", "yatra")
871 ] }),
872 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
873 bucket.prompt.toLocaleString(),
874 " ",
875 __("in", "yatra")
876 ] }),
877 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
878 bucket.completion.toLocaleString(),
879 " ",
880 __("out", "yatra")
881 ] })
882 ]
883 },
884 prov
885 ))
886 ]
887 },
888 month
889 )) })
890 ] }) })
891 ] });
892 };
893 const renderPrompts = () => {
894 var _a;
895 const rows = (promptsResp == null ? void 0 : promptsResp.data) ?? [];
896 const grouped = {};
897 for (const r of rows) {
898 (grouped[_a = r.category] || (grouped[_a] = [])).push(r);
899 }
900 const groupOrder = Object.keys(grouped);
901 const submitPrompt = (row) => {
902 const draft = promptDrafts[row.task];
903 if (!draft) return;
904 const override = {};
905 const sysTrim = draft.system.trim();
906 override.system = sysTrim === "" || sysTrim === row.default.system.trim() ? "" : draft.system;
907 const usrTrim = draft.user.trim();
908 override.user = usrTrim === "" || usrTrim === row.default.user.trim() ? "" : draft.user;
909 const mt = parseInt(draft.max_tokens, 10);
910 override.max_tokens = Number.isFinite(mt) && mt !== row.default.max_tokens ? mt : null;
911 const tp = parseFloat(draft.temperature);
912 override.temperature = Number.isFinite(tp) && tp !== row.default.temperature ? tp : null;
913 savePromptMutation.mutate({ task: row.task, override });
914 };
915 return /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
916 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
917 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
918 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Prompts", "yatra") }),
919 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
920 "Every system + user prompt the assistant uses. Edit any field — blank fields fall back to the shipped default per-field. Brand voice is auto-prepended to system prompts, so don't repeat it here.",
921 "yatra"
922 ) })
923 ] }),
924 /* @__PURE__ */ jsxRuntimeExports.jsxs(
925 Button,
926 {
927 type: "button",
928 variant: "outline",
929 onClick: () => {
930 if (window.confirm(
931 __(
932 "Reset every prompt to its shipped default? This wipes all your overrides.",
933 "yatra"
934 )
935 )) {
936 resetAllPromptsMutation.mutate();
937 }
938 },
939 disabled: resetAllPromptsMutation.isPending,
940 children: [
941 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "mr-2 h-4 w-4" }),
942 __("Reset all", "yatra")
943 ]
944 }
945 )
946 ] }) }),
947 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { children: promptsLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx(ModuleSectionSkeleton, { lines: 5 }) : rows.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500", children: __("No prompts available.", "yatra") }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
948 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50/40 p-3 text-xs text-blue-800 dark:border-blue-500/40 dark:bg-blue-900/20 dark:text-blue-200", children: [
949 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }),
950 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: __(
951 "Use {{placeholders}} to inject runtime context (e.g. {{name}}, {{extra_context}}). Removing a placeholder means the assistant won't see that field — edit with care.",
952 "yatra"
953 ) })
954 ] }),
955 groupOrder.map((cat) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
956 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400", children: cat }),
957 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-2", children: grouped[cat].map((row) => {
958 var _a2;
959 const isOpen = expandedPrompt === row.task;
960 const draft = promptDrafts[row.task] ?? {
961 system: row.default.system,
962 user: row.default.user,
963 max_tokens: String(row.default.max_tokens),
964 temperature: String(row.default.temperature)
965 };
966 const saving = savePromptMutation.isPending && ((_a2 = savePromptMutation.variables) == null ? void 0 : _a2.task) === row.task;
967 const resetting = resetPromptMutation.isPending && resetPromptMutation.variables === row.task;
968 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
969 "div",
970 {
971 className: "rounded-md border border-gray-200 dark:border-gray-700",
972 children: [
973 /* @__PURE__ */ jsxRuntimeExports.jsxs(
974 "button",
975 {
976 type: "button",
977 onClick: () => setExpandedPrompt(isOpen ? null : row.task),
978 className: "flex w-full items-start justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-800/60",
979 children: [
980 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
981 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
982 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-gray-900 dark:text-white", children: row.label }),
983 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "rounded bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: row.task }),
984 row.has_override && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", children: __("Customized", "yatra") })
985 ] }),
986 row.description && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-0.5 text-[11px] leading-snug text-gray-500 dark:text-gray-400", children: row.description })
987 ] }),
988 isOpen ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { className: "mt-1 h-4 w-4 shrink-0 text-gray-400" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "mt-1 h-4 w-4 shrink-0 text-gray-400" })
989 ]
990 }
991 ),
992 isOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3 border-t border-gray-200 p-3 dark:border-gray-700", children: [
993 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
994 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: `pr-sys-${row.task}`, children: __("System prompt", "yatra") }),
995 /* @__PURE__ */ jsxRuntimeExports.jsx(
996 "textarea",
997 {
998 id: `pr-sys-${row.task}`,
999 rows: 5,
1000 className: "mt-1 w-full rounded-md border border-gray-300 bg-white p-2 font-mono text-xs dark:border-gray-600 dark:bg-gray-800",
1001 value: draft.system,
1002 onChange: (e) => setPromptDrafts((d) => ({
1003 ...d,
1004 [row.task]: {
1005 ...draft,
1006 system: e.target.value
1007 }
1008 }))
1009 }
1010 )
1011 ] }),
1012 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1013 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: `pr-usr-${row.task}`, children: __("User prompt", "yatra") }),
1014 /* @__PURE__ */ jsxRuntimeExports.jsx(
1015 "textarea",
1016 {
1017 id: `pr-usr-${row.task}`,
1018 rows: 10,
1019 className: "mt-1 w-full rounded-md border border-gray-300 bg-white p-2 font-mono text-xs dark:border-gray-600 dark:bg-gray-800",
1020 value: draft.user,
1021 onChange: (e) => setPromptDrafts((d) => ({
1022 ...d,
1023 [row.task]: {
1024 ...draft,
1025 user: e.target.value
1026 }
1027 }))
1028 }
1029 )
1030 ] }),
1031 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
1032 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1033 /* @__PURE__ */ jsxRuntimeExports.jsxs(Label, { htmlFor: `pr-mt-${row.task}`, children: [
1034 __("Max tokens", "yatra"),
1035 " ",
1036 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-gray-400", children: [
1037 "(",
1038 __("default", "yatra"),
1039 ":",
1040 " ",
1041 row.default.max_tokens,
1042 ")"
1043 ] })
1044 ] }),
1045 /* @__PURE__ */ jsxRuntimeExports.jsx(
1046 Input,
1047 {
1048 id: `pr-mt-${row.task}`,
1049 type: "number",
1050 min: 16,
1051 max: 8e3,
1052 value: draft.max_tokens,
1053 onChange: (e) => setPromptDrafts((d) => ({
1054 ...d,
1055 [row.task]: {
1056 ...draft,
1057 max_tokens: e.target.value
1058 }
1059 }))
1060 }
1061 )
1062 ] }),
1063 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1064 /* @__PURE__ */ jsxRuntimeExports.jsxs(Label, { htmlFor: `pr-tp-${row.task}`, children: [
1065 __("Temperature", "yatra"),
1066 " ",
1067 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-gray-400", children: [
1068 "(",
1069 __("default", "yatra"),
1070 ":",
1071 " ",
1072 row.default.temperature,
1073 ")"
1074 ] })
1075 ] }),
1076 /* @__PURE__ */ jsxRuntimeExports.jsx(
1077 Input,
1078 {
1079 id: `pr-tp-${row.task}`,
1080 type: "number",
1081 min: 0,
1082 max: 2,
1083 step: 0.1,
1084 value: draft.temperature,
1085 onChange: (e) => setPromptDrafts((d) => ({
1086 ...d,
1087 [row.task]: {
1088 ...draft,
1089 temperature: e.target.value
1090 }
1091 }))
1092 }
1093 )
1094 ] })
1095 ] }),
1096 /* @__PURE__ */ jsxRuntimeExports.jsxs("details", { className: "rounded-md border border-dashed border-gray-200 px-3 py-2 text-xs dark:border-gray-700", children: [
1097 /* @__PURE__ */ jsxRuntimeExports.jsx("summary", { className: "cursor-pointer text-gray-500 hover:text-gray-700 dark:text-gray-400", children: __("Show shipped defaults", "yatra") }),
1098 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 space-y-2", children: [
1099 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1100 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400", children: __("Default system", "yatra") }),
1101 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "max-h-40 overflow-auto whitespace-pre-wrap rounded bg-gray-50 p-2 text-[11px] text-gray-700 dark:bg-gray-900 dark:text-gray-200", children: row.default.system })
1102 ] }),
1103 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1104 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400", children: __("Default user", "yatra") }),
1105 /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "max-h-60 overflow-auto whitespace-pre-wrap rounded bg-gray-50 p-2 text-[11px] text-gray-700 dark:bg-gray-900 dark:text-gray-200", children: row.default.user })
1106 ] })
1107 ] })
1108 ] }),
1109 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-1", children: [
1110 /* @__PURE__ */ jsxRuntimeExports.jsx(
1111 Button,
1112 {
1113 type: "button",
1114 onClick: () => submitPrompt(row),
1115 disabled: saving,
1116 children: saving ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1117 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
1118 __("Saving…", "yatra")
1119 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1120 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "mr-2 h-4 w-4" }),
1121 __("Save", "yatra")
1122 ] })
1123 }
1124 ),
1125 row.has_override && /* @__PURE__ */ jsxRuntimeExports.jsxs(
1126 Button,
1127 {
1128 type: "button",
1129 variant: "ghost",
1130 onClick: () => resetPromptMutation.mutate(row.task),
1131 disabled: resetting,
1132 children: [
1133 resetting ? /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "mr-2 h-4 w-4" }),
1134 __("Reset to default", "yatra")
1135 ]
1136 }
1137 )
1138 ] })
1139 ] })
1140 ]
1141 },
1142 row.task
1143 );
1144 }) })
1145 ] }, cat))
1146 ] }) })
1147 ] });
1148 };
1149 const currentSection = SECTIONS.find((s) => s.id === activeSection) ?? SECTIONS[0];
1150 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
1151 /* @__PURE__ */ jsxRuntimeExports.jsx(
1152 PageHeader,
1153 {
1154 description: __(
1155 "Inline AI generation grounded in your trip data. Bring your own key.",
1156 "yatra"
1157 )
1158 }
1159 ),
1160 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid gap-6 lg:grid-cols-[240px_1fr]", children: [
1161 /* @__PURE__ */ jsxRuntimeExports.jsxs("aside", { className: "lg:sticky lg:top-4 lg:self-start", children: [
1162 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("nav", { className: "flex flex-col gap-1", children: SECTIONS.map((s) => {
1163 const Icon = s.icon;
1164 const active = s.id === activeSection;
1165 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
1166 "button",
1167 {
1168 type: "button",
1169 onClick: () => setActiveSection(s.id),
1170 className: `flex items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors ${active ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300" : "text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800"}`,
1171 children: [
1172 /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "h-4 w-4 shrink-0" }),
1173 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 truncate", children: s.label })
1174 ]
1175 },
1176 s.id
1177 );
1178 }) }) }) }),
1179 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 px-2 text-xs text-gray-500 dark:text-gray-400", children: currentSection.description })
1180 ] }),
1181 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1182 activeSection === "keys" && renderKeys(),
1183 activeSection === "voice" && renderVoice(),
1184 activeSection === "prompts" && renderPrompts(),
1185 activeSection === "usage" && renderUsage()
1186 ] })
1187 ] })
1188 ] });
1189 };
1190 const LimitField = ({ label, hint, field, draft, setDraft, schema }) => {
1191 const range = schema[field];
1192 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1193 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: `chat-limit-${field}`, children: label }),
1194 /* @__PURE__ */ jsxRuntimeExports.jsx(
1195 Input,
1196 {
1197 id: `chat-limit-${field}`,
1198 type: "number",
1199 min: range == null ? void 0 : range.min,
1200 max: range == null ? void 0 : range.max,
1201 value: draft[field],
1202 onChange: (e) => setDraft((prev) => ({ ...prev, [field]: e.target.value }))
1203 }
1204 ),
1205 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-0.5 text-[10px] text-gray-500 dark:text-gray-400", children: [
1206 hint,
1207 range && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1208 " ",
1209 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-gray-400", children: [
1210 "(",
1211 range.min,
1212 "–",
1213 range.max,
1214 ")"
1215 ] })
1216 ] })
1217 ] })
1218 ] });
1219 };
1220 const Stat = ({ label, value }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 p-3 dark:border-gray-700", children: [
1221 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] uppercase tracking-wide text-gray-500 dark:text-gray-400", children: label }),
1222 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-1 text-lg font-semibold text-gray-900 dark:text-white", children: value })
1223 ] });
1224 function extractError(e) {
1225 var _a;
1226 const data = ((_a = e == null ? void 0 : e.response) == null ? void 0 : _a.data) ?? (e == null ? void 0 : e.data) ?? null;
1227 if (data && typeof data === "object" && typeof data.message === "string") {
1228 return data.message;
1229 }
1230 return (e == null ? void 0 : e.message) || "AI request failed.";
1231 }
1232 export {
1233 AiAssistant as default
1234 };
1235 //# sourceMappingURL=AiAssistant-Bu_zHUnt.js.map
1236