PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
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 2.0.1 All 84 releases
yatra / resources / js / components / ai / CustomerSummaryCard.tsx

CustomerSummaryCard.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.5.1, at resources/js/components/ai/CustomerSummaryCard.tsx

172 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState } from "react";
2 import { useMutation } from "@tanstack/react-query";
3 import { Sparkles, Loader2, ShieldOff, Shield, RefreshCw } from "lucide-react";
4 import { __ } from "../../lib/i18n";
5 import { aiApi } from "../../api/ai-api";
6 import {
7 isAiEligible,
8 isAiModuleEnabled,
9 isAiReady,
10 } from "../../lib/ai-availability";
11 import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
12 import { Button } from "../ui/button";
13
14 /**
15 * 3-line AI-generated snapshot of a customer record, surfaced on the
16 * ViewCustomer detail page. Renders only when the operator has the
17 * AI Assistant module on + at least one provider configured (same
18 * gate as every other AI affordance).
19 *
20 * The summary isn't auto-generated on page load — operators click
21 * `Generate summary` so the page stays fast and the AI cost is
22 * spent only when the operator actually wants the view. They can
23 * opt-in to include sensitive PII (medical, dietary, notes) per
24 * generation via a toggle — defaults to OFF so health data doesn't
25 * leak to the LLM unless the operator clearly decides it should.
26 */
27 interface Props {
28 customerId: number;
29 }
30
31 export const CustomerSummaryCard: React.FC<Props> = ({ customerId }) => {
32 const [includeSensitive, setIncludeSensitive] = useState(false);
33 const [summary, setSummary] = useState<string | null>(null);
34
35 const mutation = useMutation({
36 mutationFn: () => aiApi.getCustomerSummary(customerId, includeSensitive),
37 onSuccess: (resp) => {
38 setSummary(String(resp.data?.text ?? "").trim());
39 },
40 });
41
42 // Hard gate — don't render the card at all for non-eligible
43 // tiers / unconfigured installs. Saves a visible empty section
44 // on every customer page in the free / Personal tier.
45 // Note: early return MUST come after every hook above, otherwise
46 // the hook-count changes between renders and React's internal
47 // state machine breaks (rules-of-hooks).
48 if (!isAiEligible() || !isAiModuleEnabled()) {
49 return null;
50 }
51
52 const ready = isAiReady();
53
54 return (
55 <Card>
56 <CardHeader>
57 <div className="flex items-start justify-between gap-3">
58 <div>
59 <CardTitle className="flex items-center gap-2 text-base">
60 <Sparkles className="h-4 w-4 text-blue-600 dark:text-blue-300" />
61 {__("AI Customer Summary", "yatra")}
62 </CardTitle>
63 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
64 {__(
65 "A 3-line operator-facing snapshot — status, last trip, anything notable for ops.",
66 "yatra",
67 )}
68 </p>
69 </div>
70 {!summary && (
71 <Button
72 type="button"
73 size="sm"
74 onClick={() => mutation.mutate()}
75 disabled={mutation.isPending || !ready}
76 className="bg-blue-600 text-white hover:bg-blue-700"
77 >
78 {mutation.isPending ? (
79 <>
80 <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
81 {__("Generating…", "yatra")}
82 </>
83 ) : (
84 <>
85 <Sparkles className="mr-1.5 h-3.5 w-3.5" />
86 {__("Generate summary", "yatra")}
87 </>
88 )}
89 </Button>
90 )}
91 </div>
92 </CardHeader>
93 <CardContent className="space-y-3">
94 {!ready && (
95 <div className="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">
96 {__(
97 "Configure an AI provider key under Yatra → AI Assistant to enable this.",
98 "yatra",
99 )}
100 </div>
101 )}
102
103 {/* Sensitive-data opt-in — defaults to OFF so health/dietary
104 notes never reach the LLM unless the operator deliberately
105 flips this for the run. Hidden once we have a result so
106 it doesn't visually compete with the summary itself. */}
107 {!summary && (
108 <label className="flex cursor-pointer items-start gap-2 rounded-md border border-gray-200 p-2.5 text-xs hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/60">
109 <input
110 type="checkbox"
111 checked={includeSensitive}
112 onChange={(e) => setIncludeSensitive(e.target.checked)}
113 className="mt-0.5"
114 />
115 <div className="flex-1">
116 <div className="flex items-center gap-1.5 font-medium text-gray-900 dark:text-white">
117 {includeSensitive ? (
118 <Shield className="h-3.5 w-3.5 text-amber-500" />
119 ) : (
120 <ShieldOff className="h-3.5 w-3.5 text-gray-400" />
121 )}
122 {__(
123 "Include sensitive fields (medical, dietary, notes)",
124 "yatra",
125 )}
126 </div>
127 <div className="mt-0.5 text-gray-500 dark:text-gray-400">
128 {__(
129 "Off by default. When on, the customer's health and preference details flow to the AI for this generation only.",
130 "yatra",
131 )}
132 </div>
133 </div>
134 </label>
135 )}
136
137 {mutation.isError && (
138 <div className="rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-700 dark:border-red-500/40 dark:bg-red-900/20 dark:text-red-300">
139 {(mutation.error as any)?.response?.data?.message ||
140 (mutation.error as any)?.message ||
141 __("AI request failed.", "yatra")}
142 </div>
143 )}
144
145 {summary && (
146 <div className="space-y-2">
147 <p className="whitespace-pre-wrap text-sm leading-relaxed text-gray-900 dark:text-white">
148 {summary}
149 </p>
150 <div className="flex justify-end">
151 <Button
152 type="button"
153 variant="ghost"
154 size="sm"
155 onClick={() => {
156 setSummary(null);
157 }}
158 className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
159 >
160 <RefreshCw className="mr-1.5 h-3 w-3" />
161 {__("Regenerate", "yatra")}
162 </Button>
163 </div>
164 </div>
165 )}
166 </CardContent>
167 </Card>
168 );
169 };
170
171 export default CustomerSummaryCard;
172