@inov-ai/feedback-widget
Version:
Modern React feedback widget with surveys, analytics, and Next.js 15 support. Collect user feedback and run surveys in your SaaS applications.
847 lines (832 loc) • 70.5 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useState, useCallback, useEffect, createContext, useContext } from 'react';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var FeedbackButton = function (_a) {
var onClick = _a.onClick, config = _a.config, _b = _a.minimized, minimized = _b === void 0 ? false : _b;
var _c = config.position, position = _c === void 0 ? 'bottom-right' : _c, _d = config.primaryColor, primaryColor = _d === void 0 ? '#f97316' : _d, _e = config.buttonText, buttonText = _e === void 0 ? 'Feedback' : _e, _f = config.buttonIcon, buttonIcon = _f === void 0 ? '' : _f, _g = config.buttonRadius, buttonRadius = _g === void 0 ? '8px' : _g, _h = config.fontFamily, fontFamily = _h === void 0 ? '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' : _h, _j = config.fontSize, fontSize = _j === void 0 ? '16px' : _j;
var getPositionStyles = function () {
var baseStyles = {
position: 'fixed',
zIndex: 999999,
};
switch (position) {
case 'bottom-right':
return __assign(__assign({}, baseStyles), { bottom: '20px', right: '20px' });
case 'bottom-left':
return __assign(__assign({}, baseStyles), { bottom: '20px', left: '20px' });
case 'top-right':
return __assign(__assign({}, baseStyles), { top: '20px', right: '20px' });
case 'top-left':
return __assign(__assign({}, baseStyles), { top: '20px', left: '20px' });
default:
return __assign(__assign({}, baseStyles), { bottom: '20px', right: '20px' });
}
};
var buttonStyles = __assign(__assign({}, getPositionStyles()), { padding: minimized ? '12px' : '12px 24px', backgroundColor: primaryColor, color: 'white', border: 'none', borderRadius: buttonRadius, cursor: 'pointer', fontWeight: '600', fontFamily: fontFamily, fontSize: fontSize, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', display: 'flex', alignItems: 'center', gap: '8px', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)' });
var handleMouseEnter = function (e) {
var button = e.currentTarget;
button.style.filter = 'brightness(90%)';
button.style.transform = 'translateY(-2px)';
};
var handleMouseLeave = function (e) {
var button = e.currentTarget;
button.style.filter = 'none';
button.style.transform = 'none';
};
return (jsx("div", { className: "fb-widget-container", children: jsxs("button", { className: "fb-widget-button", onClick: onClick, style: buttonStyles, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, "aria-label": "Open feedback widget", "data-testid": "feedback-button", children: [buttonIcon && (jsx("span", { dangerouslySetInnerHTML: { __html: buttonIcon }, style: { display: 'flex', alignItems: 'center' } })), !minimized && buttonText] }) }));
};
var FeedbackForm = function (_a) {
var onSubmit = _a.onSubmit, config = _a.config, _b = _a.isLoading, isLoading = _b === void 0 ? false : _b;
var _c = useState(''), feedback = _c[0], setFeedback = _c[1];
var _d = useState(''), category = _d[0], setCategory = _d[1];
var _e = useState(), rating = _e[0]; _e[1];
var _f = config.primaryColor, primaryColor = _f === void 0 ? '#f97316' : _f, _g = config.textColor, textColor = _g === void 0 ? '#111827' : _g, _h = config.buttonRadius, buttonRadius = _h === void 0 ? '8px' : _h, _j = config.theme, theme = _j === void 0 ? 'light' : _j, _k = config.feedbackTypes, feedbackTypes = _k === void 0 ? [] : _k;
var defaultFeedbackTypes = ["Feature Request", "Bug Report", "Integration", "Question"];
var availableFeedbackTypes = feedbackTypes.length > 0 ? feedbackTypes : defaultFeedbackTypes;
var handleSubmit = function (e) { return __awaiter(void 0, void 0, void 0, function () {
var submissionData;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
e.preventDefault();
if (!feedback.trim())
return [2 /*return*/];
submissionData = {
feedback: feedback.trim(),
category: category || undefined,
rating: rating,
metadata: {
page_url: window.location.href,
user_agent: navigator.userAgent,
timestamp: new Date().toISOString(),
},
};
return [4 /*yield*/, onSubmit(submissionData)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); };
var inputStyles = {
width: '100%',
padding: '12px 16px',
border: "1px solid ".concat(theme === 'dark' ? '#374151' : '#d1d5db'),
borderRadius: buttonRadius,
backgroundColor: theme === 'dark' ? '#1f2937' : 'rgb(247, 248, 250)',
color: textColor,
fontSize: '16px',
transition: 'border-color 0.2s',
outline: 'none',
};
var buttonStyles = {
padding: '12px 32px',
backgroundColor: primaryColor,
color: 'white',
border: 'none',
borderRadius: buttonRadius,
cursor: isLoading ? 'not-allowed' : 'pointer',
fontWeight: '600',
fontSize: '16px',
transition: 'all 0.2s',
opacity: isLoading ? 0.7 : 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '48px',
};
return (jsxs("form", { onSubmit: handleSubmit, className: "fb-widget-form fb-feedback-form", style: { display: 'flex', flexDirection: 'column', gap: '24px' }, children: [jsxs("div", { className: "fb-widget-question pb-10", children: [jsx("label", { className: "fb-widget-label", style: {
fontSize: '16px',
fontWeight: '500',
color: textColor,
marginBottom: '8px',
display: 'block',
}, children: "What type of feedback do you have?" }), jsxs("select", { className: "fb-widget-select", name: "category", value: category, onChange: function (e) { return setCategory(e.target.value); }, required: true, style: __assign(__assign({}, inputStyles), { cursor: 'pointer' }), onFocus: function (e) {
e.target.style.borderColor = primaryColor;
}, onBlur: function (e) {
e.target.style.borderColor = theme === 'dark' ? '#374151' : '#d1d5db';
}, children: [jsx("option", { value: "", children: "Select Category" }), availableFeedbackTypes.map(function (type, index) { return (jsx("option", { value: type, children: type }, index)); })] })] }), jsxs("div", { className: "fb-widget-question", children: [jsx("label", { className: "fb-widget-label", style: {
fontSize: '16px',
fontWeight: '500',
color: textColor,
marginBottom: '10px',
display: 'block',
marginTop: '20px',
}, children: "Tell us what you think" }), jsx("textarea", { className: "fb-widget-textarea", name: "feedback", value: feedback, onChange: function (e) { return setFeedback(e.target.value); }, placeholder: "Share your thoughts...", required: true, rows: 4, style: __assign(__assign({}, inputStyles), { resize: 'vertical', minHeight: '100px', marginBottom: '16px' }), onFocus: function (e) {
e.target.style.borderColor = primaryColor;
}, onBlur: function (e) {
e.target.style.borderColor = theme === 'dark' ? '#374151' : '#d1d5db';
} })] }), jsx("button", { type: "submit", className: "fb-widget-submit", disabled: !feedback.trim() || isLoading, style: buttonStyles, onMouseEnter: function (e) {
if (!isLoading && feedback.trim()) {
e.currentTarget.style.filter = 'brightness(90%)';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}, onMouseLeave: function (e) {
e.currentTarget.style.filter = 'none';
e.currentTarget.style.transform = 'none';
}, children: isLoading ? (jsxs(Fragment, { children: [jsx("div", { style: {
width: '20px',
height: '20px',
border: '2px solid white',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginRight: '8px',
} }), "Submitting..."] })) : ('Submit Feedback') }), jsx("style", { children: "\n @keyframes spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n }\n " })] }));
};
var SurveyForm = function (_a) {
var survey = _a.survey, onSubmit = _a.onSubmit, config = _a.config, _b = _a.isLoading, isLoading = _b === void 0 ? false : _b;
var _c = useState({}), answers = _c[0], setAnswers = _c[1];
// Early return if survey is invalid
if (!survey || !survey.questions || !Array.isArray(survey.questions)) {
return (jsx("div", { style: {
padding: '32px',
textAlign: 'center',
color: '#ef4444',
}, children: "Invalid survey format. Please contact support." }));
}
var _d = config.primaryColor, primaryColor = _d === void 0 ? '#f97316' : _d, _e = config.textColor, textColor = _e === void 0 ? '#111827' : _e, _f = config.buttonRadius, buttonRadius = _f === void 0 ? '8px' : _f, _g = config.theme, theme = _g === void 0 ? 'light' : _g;
var handleSubmit = function (e) { return __awaiter(void 0, void 0, void 0, function () {
var requiredQuestions, missingAnswers, formattedAnswers;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
e.preventDefault();
requiredQuestions = survey.questions.filter(function (q) { return q.required; });
missingAnswers = requiredQuestions.filter(function (q) { return !answers[q.id]; });
if (missingAnswers.length > 0) {
alert('Please answer all required questions.');
return [2 /*return*/];
}
formattedAnswers = Object.entries(answers).map(function (_a) {
var questionId = _a[0], answer = _a[1];
return ({
question_id: parseInt(questionId),
answer: answer,
});
});
return [4 /*yield*/, onSubmit(survey.id, formattedAnswers)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); };
var updateAnswer = function (questionId, answer) {
setAnswers(function (prev) {
var _a;
return (__assign(__assign({}, prev), (_a = {}, _a[questionId] = answer, _a)));
});
};
var renderQuestion = function (question) {
var inputStyles = {
width: '100%',
padding: '12px 16px',
border: "1px solid ".concat(theme === 'dark' ? '#374151' : '#d1d5db'),
borderRadius: buttonRadius,
backgroundColor: theme === 'dark' ? '#1f2937' : '#ffffff',
color: textColor,
fontSize: '16px',
transition: 'border-color 0.2s',
outline: 'none',
};
var focusHandler = function (e) {
e.target.style.borderColor = primaryColor;
};
var blurHandler = function (e) {
e.target.style.borderColor = theme === 'dark' ? '#374151' : '#d1d5db';
};
switch (question.question_type) {
case 'rating':
var maxRating = question.max_rating || 5;
if (question.options && question.options.length > 0) {
// Custom rating options from API
return (jsxs("select", { className: "fb-widget-select", name: "q_".concat(question.id), id: "q_".concat(question.id), value: answers[question.id] || '', onChange: function (e) { return updateAnswer(question.id, e.target.value); }, required: question.required, style: __assign(__assign({}, inputStyles), { cursor: 'pointer' }), onFocus: focusHandler, onBlur: blurHandler, children: [jsx("option", { value: "", children: "Select Rating" }), question.options.map(function (option, index) {
var optionValue = typeof option === 'string' ? option : option.value;
var optionLabel = typeof option === 'string' ? option : option.label;
return (jsx("option", { value: optionValue, children: optionLabel }, index));
})] }));
}
else {
// Default 1-5 star rating
var currentRating_1 = parseInt(answers[question.id]) || 0;
return (jsxs("div", { style: { display: 'flex', gap: '8px', alignItems: 'center' }, children: [Array.from({ length: maxRating }, function (_, i) {
var rating = i + 1;
return (jsx("button", { type: "button", onClick: function () { return updateAnswer(question.id, rating.toString()); }, style: {
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '24px',
color: rating <= currentRating_1 ? primaryColor : (theme === 'dark' ? '#4B5563' : '#d1d5db'),
transition: 'color 0.2s',
padding: '4px',
}, children: "\u2605" }, rating));
}), currentRating_1 > 0 && (jsxs("span", { style: { marginLeft: '8px', fontSize: '14px', color: textColor, opacity: 0.7 }, children: [currentRating_1, "/", maxRating] }))] }));
}
case 'text':
return (jsx("textarea", { className: "fb-widget-textarea", name: "q_".concat(question.id), id: "q_".concat(question.id), value: answers[question.id] || '', onChange: function (e) { return updateAnswer(question.id, e.target.value); }, placeholder: "Share your thoughts...", required: question.required, rows: 3, style: __assign(__assign({}, inputStyles), { resize: 'vertical', minHeight: '80px' }), onFocus: focusHandler, onBlur: blurHandler }));
case 'multiple_choice':
if (question.options && question.options.length > 0) {
return (jsxs("select", { className: "fb-widget-select", name: "q_".concat(question.id), id: "q_".concat(question.id), value: answers[question.id] || '', onChange: function (e) { return updateAnswer(question.id, e.target.value); }, required: question.required, style: __assign(__assign({}, inputStyles), { cursor: 'pointer' }), onFocus: focusHandler, onBlur: blurHandler, children: [jsx("option", { value: "", children: "Select Answer" }), question.options.map(function (option, index) {
var optionValue = typeof option === 'string' ? option : option.value;
var optionLabel = typeof option === 'string' ? option : option.label;
return (jsx("option", { value: optionValue, children: optionLabel }, index));
})] }));
}
else {
// Fallback to text input if no options
return (jsx("input", { className: "fb-widget-input", type: "text", name: "q_".concat(question.id), id: "q_".concat(question.id), value: answers[question.id] || '', onChange: function (e) { return updateAnswer(question.id, e.target.value); }, required: question.required, style: inputStyles, onFocus: focusHandler, onBlur: blurHandler }));
}
default:
return (jsx("input", { className: "fb-widget-input", type: "text", name: "q_".concat(question.id), id: "q_".concat(question.id), value: answers[question.id] || '', onChange: function (e) { return updateAnswer(question.id, e.target.value); }, required: question.required, style: inputStyles, onFocus: focusHandler, onBlur: blurHandler }));
}
};
var buttonStyles = {
padding: '12px 32px',
backgroundColor: primaryColor,
color: 'white',
border: 'none',
borderRadius: buttonRadius,
cursor: isLoading ? 'not-allowed' : 'pointer',
fontWeight: '600',
fontSize: '16px',
transition: 'all 0.2s',
opacity: isLoading ? 0.7 : 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '48px',
};
return (jsxs("form", { className: "fb-widget-form fb-survey-form ".concat(survey.questions.length > 3 ? 'fb-survey-scrollable' : ''), "data-survey-id": survey.id, onSubmit: handleSubmit, style: { display: 'flex', flexDirection: 'column', maxHeight: '100%' }, children: [jsxs("div", { className: "fb-widget-survey-header", style: { marginBottom: '16px' }, children: [jsx("h3", { className: "fb-widget-survey-title", style: {
fontSize: '24px',
fontWeight: '600',
color: textColor,
margin: '0 0 8px',
}, children: survey.title }), jsx("p", { className: "fb-widget-survey-description", style: {
fontSize: '16px',
color: textColor,
opacity: 0.7,
margin: 0,
}, children: survey.description })] }), jsx("div", { className: "fb-widget-questions-container ".concat(survey.questions.length > 3 ? 'scrollable' : ''), style: __assign({ marginBottom: '16px' }, (survey.questions.length > 3 ? {
maxHeight: '40vh',
overflowY: 'auto',
paddingRight: '10px',
marginRight: '-10px',
borderTop: "1px solid ".concat(theme === 'dark' ? '#374151' : '#e5e7eb'),
borderBottom: "1px solid ".concat(theme === 'dark' ? '#374151' : '#e5e7eb'),
paddingTop: '16px',
paddingBottom: '16px',
} : {})), children: survey.questions.map(function (question) { return (jsxs("div", { className: "fb-widget-question", style: { marginBottom: '24px' }, children: [jsxs("label", { className: "fb-widget-label", htmlFor: "q_".concat(question.id), style: {
fontSize: '16px',
fontWeight: '500',
color: textColor,
marginBottom: '8px',
display: 'block',
}, children: [question.question_text, question.required && jsx("span", { style: { color: '#ef4444', marginLeft: '4px' }, children: "*" })] }), renderQuestion(question)] }, question.id)); }) }), jsx("div", { className: "fb-widget-survey-footer", style: {
paddingTop: '16px',
display: 'flex',
justifyContent: 'flex-end',
}, children: jsx("button", { type: "submit", className: "fb-widget-submit", disabled: isLoading, style: buttonStyles, onMouseEnter: function (e) {
if (!isLoading) {
e.currentTarget.style.filter = 'brightness(90%)';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}, onMouseLeave: function (e) {
e.currentTarget.style.filter = 'none';
e.currentTarget.style.transform = 'none';
}, children: isLoading ? (jsxs(Fragment, { children: [jsx("div", { style: {
width: '20px',
height: '20px',
border: '2px solid white',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginRight: '8px',
} }), "Submitting..."] })) : ('Submit Survey') }) }), jsx("style", { children: "\n @keyframes spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n }\n " })] }));
};
var SuccessMessage = function () {
return (jsxs("div", { className: "fb-widget-success", style: {
textAlign: 'center',
padding: '24px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '200px',
}, children: [jsx("div", { className: "fb-widget-success-icon", style: {
color: '#22c55e',
fontSize: '48px',
marginBottom: '16px',
animation: 'checkmark 0.6s ease-in-out',
}, children: "\u2713" }), jsx("h3", { style: {
fontSize: '24px',
fontWeight: '600',
margin: '0 0 8px',
color: 'inherit',
}, children: "Thank you for your feedback!" }), jsx("p", { style: {
fontSize: '16px',
color: 'inherit',
opacity: 0.7,
margin: '0 0 24px',
}, children: "Your response has been recorded." }), jsx("div", { className: "fb-widget-powered-by", style: {
fontSize: '12px',
color: 'inherit',
opacity: 0.5,
textAlign: 'center',
}, children: "Powered by inov-ai" }), jsx("style", { children: "\n @keyframes checkmark {\n 0% {\n transform: scale(0);\n opacity: 0;\n }\n 50% {\n transform: scale(1.2);\n }\n 100% {\n transform: scale(1);\n opacity: 1;\n }\n }\n " })] }));
};
var API_BASE = 'https://api.inov-ai.tech';
var useFeedbackApi = function (config) {
var getFeedbackMetadata = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () {
var getClientIP;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
getClientIP = function () { return __awaiter(void 0, void 0, void 0, function () {
var response, data, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 3, , 4]);
return [4 /*yield*/, fetch('https://api.ipify.org?format=json')];
case 1:
response = _a.sent();
return [4 /*yield*/, response.json()];
case 2:
data = _a.sent();
return [2 /*return*/, data.ip];
case 3:
error_1 = _a.sent();
console.warn('Failed to get client IP:', error_1);
return [2 /*return*/, '0.0.0.0'];
case 4: return [2 /*return*/];
}
});
}); };
_a = {
page_url: window.location.href,
user_agent: navigator.userAgent
};
return [4 /*yield*/, getClientIP()];
case 1: return [2 /*return*/, (_a.ip = _b.sent(),
_a.timestamp = new Date().toISOString(),
_a.referrer = document.referrer,
_a.viewport = {
width: window.innerWidth,
height: window.innerHeight,
},
_a.screen = {
width: window.screen.width,
height: window.screen.height,
},
_a)];
}
});
}); }, []);
var submitFeedback = useCallback(function (data) { return __awaiter(void 0, void 0, void 0, function () {
var metadata, payload, response, errorData;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getFeedbackMetadata()];
case 1:
metadata = _a.sent();
payload = {
site_key: config.siteKey,
feedback: data.feedback,
category: data.category,
rating: data.rating,
feedback_metadata: metadata,
processed: false,
};
return [4 /*yield*/, fetch("".concat(API_BASE, "/survey/raw-feedback"), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})];
case 2:
response = _a.sent();
if (!!response.ok) return [3 /*break*/, 4];
return [4 /*yield*/, response.json().catch(function () { return ({}); })];
case 3:
errorData = _a.sent();
throw new Error(errorData.message || 'Failed to submit feedback');
case 4:
// Mark as submitted for frequency control
if (config.surveyFrequency === 'once') {
localStorage.setItem('inov-ai-feedback-submitted', 'true');
}
return [2 /*return*/];
}
});
}); }, [config.siteKey, config.surveyFrequency, getFeedbackMetadata]);
var submitSurvey = useCallback(function (surveyId, answers) { return __awaiter(void 0, void 0, void 0, function () {
var metadata, payload, response, errorData;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getFeedbackMetadata()];
case 1:
metadata = _a.sent();
payload = {
survey_id: surveyId,
site_key: config.siteKey,
answers: answers,
feedback_metadata: metadata,
};
return [4 /*yield*/, fetch("".concat(API_BASE, "/survey/survey-response"), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})];
case 2:
response = _a.sent();
if (!!response.ok) return [3 /*break*/, 4];
return [4 /*yield*/, response.json().catch(function () { return ({}); })];
case 3:
errorData = _a.sent();
throw new Error(errorData.message || 'Failed to submit survey');
case 4:
// Mark as submitted for frequency control
if (config.surveyFrequency === 'once') {
localStorage.setItem('inov-ai-feedback-submitted', 'true');
}
return [2 /*return*/];
}
});
}); }, [config.siteKey, config.surveyFrequency, getFeedbackMetadata]);
var fetchSurvey = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () {
var response, surveys, activeSurveys, selectedSurvey, error_2;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 3, , 4]);
return [4 /*yield*/, fetch("".concat(API_BASE, "/survey?site_key=").concat(encodeURIComponent(config.siteKey)), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})];
case 1:
response = _a.sent();
if (!response.ok) {
// Don't throw error for survey fetch - it's optional
console.warn('No survey available or failed to fetch survey');
return [2 /*return*/, null];
}
return [4 /*yield*/, response.json()];
case 2:
surveys = _a.sent();
// Debug: Log what we got from the API to help troubleshoot
console.log("Survey API response:", surveys);
if (!surveys) {
console.log("No survey data received");
return [2 /*return*/, null];
}
activeSurveys = [];
if (Array.isArray(surveys)) {
// If surveys is an array, filter for active surveys
activeSurveys = surveys.filter(function (survey) { return survey && survey.is_active === true; });
console.log("Found ".concat(activeSurveys.length, " active surveys out of ").concat(surveys.length, " total"));
}
else if (typeof surveys === 'object') {
// If surveys is a single object, check if it's active
activeSurveys = surveys.is_active === true ? [surveys] : [];
console.log("Single survey object, active status: ".concat(surveys.is_active));
}
else {
console.error("Unexpected survey data format:", surveys);
return [2 /*return*/, null];
}
if (activeSurveys.length === 0) {
console.log("No active surveys available at the moment");
return [2 /*return*/, null];
}
selectedSurvey = activeSurveys[0];
// Debug: Log the selected survey
console.log("Selected active survey:", selectedSurvey);
// Make sure the survey has questions before returning
if (!selectedSurvey || !selectedSurvey.questions || !Array.isArray(selectedSurvey.questions)) {
console.error("Invalid survey format:", selectedSurvey);
return [2 /*return*/, null];
}
return [2 /*return*/, selectedSurvey];
case 3:
error_2 = _a.sent();
console.warn('Failed to fetch survey:', error_2);
return [2 /*return*/, null];
case 4: return [2 /*return*/];
}
});
}); }, [config.siteKey]);
return {
submitFeedback: submitFeedback,
submitSurvey: submitSurvey,
fetchSurvey: fetchSurvey,
getFeedbackMetadata: getFeedbackMetadata,
};
};
var FeedbackModal = function (_a) {
var isOpen = _a.isOpen, onClose = _a.onClose, config = _a.config;
var _b = useState('feedback'), activeTab = _b[0], setActiveTab = _b[1];
var _c = useState(false), isSubmitted = _c[0], setIsSubmitted = _c[1];
var _d = useState(null), error = _d[0], setError = _d[1];
var _e = useState(null), currentSurvey = _e[0], setCurrentSurvey = _e[1];
var _f = useState(false), isLoading = _f[0], setIsLoading = _f[1];
var _g = useFeedbackApi(config), submitFeedback = _g.submitFeedback, submitSurvey = _g.submitSurvey, fetchSurvey = _g.fetchSurvey;
var _h = config.backgroundColor, backgroundColor = _h === void 0 ? 'rgb(247, 248, 250)' : _h, _j = config.textColor, textColor = _j === void 0 ? '#111827' : _j, _k = config.primaryColor, primaryColor = _k === void 0 ? '#f97316' : _k, _l = config.buttonRadius, buttonRadius = _l === void 0 ? '8px' : _l, _m = config.fontFamily, fontFamily = _m === void 0 ? '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' : _m, _o = config.fontSize, fontSize = _o === void 0 ? '16px' : _o, _p = config.theme, theme = _p === void 0 ? 'light' : _p, _q = config.animation, animation = _q === void 0 ? 'slide' : _q;
// Fetch survey on mount with retry
useEffect(function () {
var retryCount = 0;
var maxRetries = 3;
var fetchSurveyWithRetry = function () { return __awaiter(void 0, void 0, void 0, function () {
var survey, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, fetchSurvey()];
case 1:
survey = _a.sent();
setCurrentSurvey(survey);
return [3 /*break*/, 3];
case 2:
error_1 = _a.sent();
console.warn("Survey fetch attempt ".concat(retryCount + 1, " failed:"), error_1);
retryCount++;
if (retryCount < maxRetries) {
// Retry after delay
setTimeout(fetchSurveyWithRetry, 1000 * retryCount);
}
else {
console.warn('Failed to fetch survey after maximum retries');
// Don't set error here as surveys are optional
}
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); };
if (isOpen) {
fetchSurveyWithRetry();
}
}, [isOpen, fetchSurvey]);
// Handle escape key
useEffect(function () {
var handleEscape = function (e) {
if (e.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return function () {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
var handleFeedbackSubmit = useCallback(function (data) { return __awaiter(void 0, void 0, void 0, function () {
var err_1, errorMessage;
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
setIsLoading(true);
setError(null);
_c.label = 1;
case 1:
_c.trys.push([1, 3, 4, 5]);
return [4 /*yield*/, submitFeedback(data)];
case 2:
_c.sent();
setIsSubmitted(true);
(_a = config.onSubmit) === null || _a === void 0 ? void 0 : _a.call(config, data);
// Auto-close after success
setTimeout(function () {
onClose();
setIsSubmitted(false);
}, 2000);
return [3 /*break*/, 5];
case 3:
err_1 = _c.sent();
errorMessage = err_1 instanceof Error ? err_1.message : 'Failed to submit feedback';
setError(errorMessage);
(_b = config.onError) === null || _b === void 0 ? void 0 : _b.call(config, err_1 instanceof Error ? err_1 : new Error(errorMessage));
return [3 /*break*/, 5];
case 4:
setIsLoading(false);
return [7 /*endfinally*/];
case 5: return [2 /*return*/];
}
});
}); }, [submitFeedback, config, onClose]);
var handleSurveySubmit = useCallback(function (surveyId, answers) { return __awaiter(void 0, void 0, void 0, function () {
var err_2, errorMessage;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
setIsLoading(true);
setError(null);
_b.label = 1;
case 1:
_b.trys.push([1, 3, 4, 5]);
return [4 /*yield*/, submitSurvey(surveyId, answers)];
case 2:
_b.sent();
setIsSubmitted(true);
// Auto-close after success
setTimeout(function () {
onClose();
setIsSubmitted(false);
}, 2000);
return [3 /*break*/, 5];
case 3:
err_2 = _b.sent();
errorMessage = err_2 instanceof Error ? err_2.message : 'Failed to submit survey';
setError(errorMessage);
(_a = config.onError) === null || _a === void 0 ? void 0 : _a.call(config, err_2 instanceof Error ? err_2 : new Error(errorMessage));
return [3 /*break*/, 5];
case 4:
setIsLoading(false);
return [7 /*endfinally*/];
case 5: return [2 /*return*/];
}
});
}); }, [submitSurvey, config, onClose]);
var handleTabChange = function (tab) {
setActiveTab(tab);
setError(null);
// If switching to survey tab and no survey loaded, try to fetch it
if (tab === 'survey' && !currentSurvey && !isLoading) {
setIsLoading(true);
fetchSurvey()
.then(function (survey) {
setCurrentSurvey(survey);
setIsLoading(false);
})
.catch(function (err) {
console.error('Failed to fetch survey:', err);
setError('Failed to load survey. Please try again.');
setIsLoading(false);
// Switch back to feedback tab if survey fails to load
setActiveTab('feedback');
});
}
};
if (!isOpen)
return null;
// Generate animation styles
var getAnimationStyles = function () {
switch (animation) {
case 'slide':
return {
animationName: 'slideIn',
animationDuration: '0.4s',
animationTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
};
case 'fade':
return {
animationName: 'fadeIn',
animationDuration: '0.4s',
animationTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
};
default:
return {};
}
};
return (jsxs(Fragment, { children: [jsx("style", { children: "\n @keyframes slideIn {\n from { transform: translate(-50%, -40%); opacity: 0; }\n to { transform: translate(-50%, -50%); opacity: 1; }\n }\n @keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n " }), jsx("div", { className: "fb-widget-overlay", onClick: onClose, style: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backdropFilter: 'blur(4px)',
background: 'rgba(0, 0, 0, 0.1)',
zIndex: 999999,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
} }), jsxs("div", { className: "fb-widget-modal", style: __assign({ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: backgroundColor, color: textColor, padding: '32px', borderRadius: '16px', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', width: '90%', maxWidth: '500px', maxHeight: '85vh', zIndex: 1000000, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: fontFamily, fontSize: fontSize }, getAnimationStyles()), children: [jsx("button", { className: "fb-widget-close", onClick: onClose, style: {
position: 'absolute',
top: '16px',
right: '16px',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '8px',
color: textColor,
fontSize: '24px',
transition: 'all 0.2s',
}, onMouseEnter: function (e) {
e.currentTarget.style.transform = 'rotate(90deg)';
}, onMouseLeave: function (e) {
e.currentTarget.style.transform = 'none';
}, children: "\u00D7" }), isSubmitted ? (jsx(SuccessMessage, {})) : (jsxs(Fragment, { children: [jsx("h2", { className: "fb-widget-title", style: {
fontSize: '32px',
fontWeight: 600,
color: textColor,
margin: '0 0 8px',
}, children: "Give us feedback" }), jsx("p", { className: "fb-widget-subtitle", style: {
fontSize: '16px',
color: textColor,
opacity: 0.7,
margin: '0 0 32px',
}, children: "Tell us how we could make the product more useful for you." }), jsxs("div", { className: "fb-widget-tabs", style: {
display: 'flex',
gap: '12px',
marginBottom: '32px',
}, children: [jsx("button", { className: "fb-widget-tab ".concat(activeTab === 'feedback' ? 'active' : ''), onClick: function () { return handleTabChange('feedback'); }, style: {
padding: '12px 24px',
background: activeTab === 'feedback'
? primaryColor
: theme === 'dark' ? '#374151' : '#f3f4f6',
border: 'none',
borderRadius: buttonRadius,
cursor: 'pointer',
color: activeTab === 'feedback' ? 'white' : textColor,
fontWeight: 500,
fontSize: '16px',
transition: 'all 0.2s',
}, onMouseEnter: function (e) {
if (activeTab !== 'feedback') {
e.currentTarget.style.filter = 'brightness(95%)';
}
}, onMouseLeave: function (e) {
e.current