@exotel-npm-dev/exotel-ai-assist 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { Suggestion, BotConfig } from "../../types";
2
2
  import "../../styles/index.css";
3
- export declare function SuggestionsTab({ suggestions, connected, botConfig, }: {
3
+ export declare function SuggestionsTab({ suggestions, connected, botConfig, sendSuggestionFeedback, }: {
4
4
  suggestions: Suggestion[];
5
5
  connected: boolean;
6
6
  botConfig: BotConfig | null;
7
+ sendSuggestionFeedback: (sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason?: string | null) => boolean;
7
8
  }): JSX.Element;
@@ -19,6 +19,7 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
19
19
  destroy(): void;
20
20
  getStatus(): ConnectionStatus;
21
21
  getStreamState(): StreamState | null;
22
+ sendSuggestionFeedback(sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason?: string | null): boolean;
22
23
  private _ensureTransport;
23
24
  private _handleTransportMessage;
24
25
  private _scheduleReconnect;
@@ -1667,6 +1667,25 @@ class ExotelAIAssistController extends EventEmitter {
1667
1667
  getStreamState() {
1668
1668
  return this._streamState;
1669
1669
  }
1670
+ sendSuggestionFeedback(sequence, feedbackType, badFeedbackReason = null) {
1671
+ if (!this.transport || this.status !== "connected") {
1672
+ console.warn("[ExotelAIAssist] Cannot send feedback: not connected");
1673
+ return false;
1674
+ }
1675
+ const message = {
1676
+ type: "suggestion_feedback",
1677
+ sequence,
1678
+ feedback_type: feedbackType,
1679
+ bad_feedback_reason: badFeedbackReason
1680
+ };
1681
+ try {
1682
+ this.transport.send(JSON.stringify(message));
1683
+ return true;
1684
+ } catch (error) {
1685
+ console.error("[ExotelAIAssist] Failed to send feedback:", error);
1686
+ return false;
1687
+ }
1688
+ }
1670
1689
  _ensureTransport() {
1671
1690
  if (this.transport) return;
1672
1691
  this.transport = createTransport(Utils.hash(this.params));
@@ -1789,17 +1808,27 @@ class ExotelAIAssistController extends EventEmitter {
1789
1808
  });
1790
1809
  this.emit("transcript", lines);
1791
1810
  }
1792
- if (event.event_type === "suggestion" && event.value) {
1793
- const suggestion = {
1794
- id: Utils.getUniqueId(),
1795
- value: event.value,
1796
- timestamp: now
1797
- };
1798
- this.emit("suggestion", suggestion);
1811
+ if (event.event_type === "suggestion") {
1812
+ const val = event.value;
1813
+ const text = typeof val === "object" && val !== null && "text" in val ? val.text : typeof val === "string" ? val : event.text;
1814
+ const sequence = typeof val === "object" && val !== null && "sequence" in val ? val.sequence : 0;
1815
+ if (text) {
1816
+ const suggestion = {
1817
+ id: Utils.getUniqueId(),
1818
+ text,
1819
+ value: text,
1820
+ timestamp: now,
1821
+ sequence,
1822
+ feedbackType: null,
1823
+ badFeedbackReason: null
1824
+ };
1825
+ this.emit("suggestion", suggestion);
1826
+ }
1799
1827
  }
1800
1828
  if (event.event_type === "sentiment" && event.value) {
1829
+ const sentimentValue = typeof event.value === "string" ? event.value : event.value.text;
1801
1830
  const sentiment = {
1802
- label: event.value.toLowerCase(),
1831
+ label: sentimentValue.toLowerCase(),
1803
1832
  timestamp: now
1804
1833
  };
1805
1834
  this.emit("sentiment", sentiment);
@@ -25,6 +25,7 @@ export interface UseExotelAIAssistReturn {
25
25
  connect: () => void;
26
26
  disconnect: () => void;
27
27
  setParams: (patch: Partial<ExotelAIAssistParams>) => void;
28
+ sendSuggestionFeedback: (sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason?: string | null) => boolean;
28
29
  }
29
30
  /** Internal extended return — adds botConfig for the UI component only. */
30
31
  export interface UseExotelAIAssistInternalReturn extends UseExotelAIAssistReturn {
package/dist/index.js CHANGED
@@ -19294,6 +19294,30 @@ const Smile = createLucideIcon$1("Smile", [["circle", {
19294
19294
  key: "1p4y9e"
19295
19295
  }]]);
19296
19296
 
19297
+ /**
19298
+ * lucide-react v0.0.1 - ISC
19299
+ */
19300
+
19301
+ const ThumbsDown = createLucideIcon$1("ThumbsDown", [["path", {
19302
+ d: "M17 14V2",
19303
+ key: "8ymqnk"
19304
+ }], ["path", {
19305
+ d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z",
19306
+ key: "s6e0r"
19307
+ }]]);
19308
+
19309
+ /**
19310
+ * lucide-react v0.0.1 - ISC
19311
+ */
19312
+
19313
+ const ThumbsUp = createLucideIcon$1("ThumbsUp", [["path", {
19314
+ d: "M7 10v12",
19315
+ key: "1qc93n"
19316
+ }], ["path", {
19317
+ d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z",
19318
+ key: "y3tblf"
19319
+ }]]);
19320
+
19297
19321
  const MAX_VISIBLE_TOASTS = 5;
19298
19322
  const AUTO_DISMISS_MS = 3000;
19299
19323
  const ToastContext = reactExports.createContext(null);
@@ -21085,6 +21109,25 @@ class ExotelAIAssistController extends EventEmitter {
21085
21109
  getStreamState() {
21086
21110
  return this._streamState;
21087
21111
  }
21112
+ sendSuggestionFeedback(sequence, feedbackType, badFeedbackReason = null) {
21113
+ if (!this.transport || this.status !== "connected") {
21114
+ console.warn("[ExotelAIAssist] Cannot send feedback: not connected");
21115
+ return false;
21116
+ }
21117
+ const message = {
21118
+ type: "suggestion_feedback",
21119
+ sequence,
21120
+ feedback_type: feedbackType,
21121
+ bad_feedback_reason: badFeedbackReason
21122
+ };
21123
+ try {
21124
+ this.transport.send(JSON.stringify(message));
21125
+ return true;
21126
+ } catch (error) {
21127
+ console.error("[ExotelAIAssist] Failed to send feedback:", error);
21128
+ return false;
21129
+ }
21130
+ }
21088
21131
  _ensureTransport() {
21089
21132
  if (this.transport) return;
21090
21133
  this.transport = createTransport(Utils.hash(this.params));
@@ -21207,17 +21250,27 @@ class ExotelAIAssistController extends EventEmitter {
21207
21250
  });
21208
21251
  this.emit("transcript", lines);
21209
21252
  }
21210
- if (event.event_type === "suggestion" && event.value) {
21211
- const suggestion = {
21212
- id: Utils.getUniqueId(),
21213
- value: event.value,
21214
- timestamp: now
21215
- };
21216
- this.emit("suggestion", suggestion);
21253
+ if (event.event_type === "suggestion") {
21254
+ const val = event.value;
21255
+ const text = typeof val === "object" && val !== null && "text" in val ? val.text : typeof val === "string" ? val : event.text;
21256
+ const sequence = typeof val === "object" && val !== null && "sequence" in val ? val.sequence : 0;
21257
+ if (text) {
21258
+ const suggestion = {
21259
+ id: Utils.getUniqueId(),
21260
+ text,
21261
+ value: text,
21262
+ timestamp: now,
21263
+ sequence,
21264
+ feedbackType: null,
21265
+ badFeedbackReason: null
21266
+ };
21267
+ this.emit("suggestion", suggestion);
21268
+ }
21217
21269
  }
21218
21270
  if (event.event_type === "sentiment" && event.value) {
21271
+ const sentimentValue = typeof event.value === "string" ? event.value : event.value.text;
21219
21272
  const sentiment = {
21220
- label: event.value.toLowerCase(),
21273
+ label: sentimentValue.toLowerCase(),
21221
21274
  timestamp: now
21222
21275
  };
21223
21276
  this.emit("sentiment", sentiment);
@@ -21293,9 +21346,20 @@ function useExotelAIAssist(params) {
21293
21346
  var _a;
21294
21347
  return (_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.setParams(patch);
21295
21348
  }, []);
21349
+ const sendSuggestionFeedback = reactExports.useCallback((sequence, feedbackType, badFeedbackReason = null) => {
21350
+ var _a, _b;
21351
+ const sent = (_b = (_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.sendSuggestionFeedback(sequence, feedbackType, badFeedbackReason)) !== null && _b !== void 0 ? _b : false;
21352
+ if (sent) {
21353
+ setSuggestions(prev => prev.map(s => s.sequence === sequence ? Object.assign(Object.assign({}, s), {
21354
+ feedbackType,
21355
+ badFeedbackReason
21356
+ }) : s));
21357
+ }
21358
+ return sent;
21359
+ }, []);
21296
21360
  return {
21297
- isReady,
21298
21361
  streamState,
21362
+ isReady,
21299
21363
  suggestions,
21300
21364
  transcripts,
21301
21365
  sentiment,
@@ -21303,7 +21367,8 @@ function useExotelAIAssist(params) {
21303
21367
  lastError,
21304
21368
  connect,
21305
21369
  disconnect,
21306
- setParams
21370
+ setParams,
21371
+ sendSuggestionFeedback
21307
21372
  };
21308
21373
  }
21309
21374
 
@@ -21332,7 +21397,7 @@ function styleInject(css, ref) {
21332
21397
  }
21333
21398
  }
21334
21399
 
21335
- var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{flex:1;min-height:0;overflow:hidden}.oa-tabs-content[data-state=inactive]{display:none!important}.oa-tabs-content[data-state=active]{display:flex;flex-direction:column}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px;text-align:left}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px;text-align:left}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover{background:#f9fafb;color:#374151}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-stream-overlay{display:flex;flex:1;flex-direction:column;min-height:0}.oa-stream-banner{align-items:flex-start;background:#fef3c7;border:1px solid #fde68a;border-radius:8px;color:#92400e;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-stream-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-stream-banner-text{flex:1}.oa-stream-empty{flex:1;gap:16px;justify-content:center;min-height:0}.oa-stream-empty,.oa-stream-empty-text{align-items:center;display:flex;flex-direction:column}.oa-stream-empty-text{gap:6px;text-align:center}.oa-stream-empty-title{color:#111827;font-size:16px;font-weight:700;line-height:20px}.oa-stream-empty-subtitle{color:#6b7280;font-size:14px;line-height:20px}.oa-throttle-banner{align-items:flex-start;background:#fcefee;border:1px solid #e8cecb;border-radius:8px;color:#833d09;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-throttle-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-throttle-banner-text{flex:1}.oa-starting-loader{align-items:center;display:flex;flex:1;flex-direction:column;gap:16px;height:100%;justify-content:center;min-height:0}.oa-starting-spinner{animation:oa-spinner-rotate 1.4s linear infinite;height:48px;width:48px}.oa-starting-spinner-track{stroke:#e5e7eb}.oa-starting-spinner-arc{stroke:#2563eb;stroke-dasharray:80,126;stroke-dashoffset:0;animation:oa-spinner-dash 1.4s ease-in-out infinite}@keyframes oa-spinner-rotate{to{transform:rotate(1turn)}}@keyframes oa-spinner-dash{0%{stroke-dasharray:1,126;stroke-dashoffset:0}50%{stroke-dasharray:80,126;stroke-dashoffset:-35}to{stroke-dasharray:80,126;stroke-dashoffset:-124}}.oa-starting-loader-text{color:#111827;font-size:16px;font-weight:600;line-height:20px}.oa-muted{color:#6b7280}";
21400
+ var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{flex:1;min-height:0;overflow:hidden}.oa-tabs-content[data-state=inactive]{display:none!important}.oa-tabs-content[data-state=active]{display:flex;flex-direction:column}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px;text-align:left}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px;text-align:left}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon,.oa-feedback-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover,.oa-feedback-icon:hover{background:#f9fafb;color:#374151}.oa-feedback-icon--positive{background:#dcfce7!important;border-color:#86efac!important;color:#16a34a!important}.oa-feedback-icon--negative{background:#fee2e2!important;border-color:#fca5a5!important;color:#dc2626!important}.oa-feedback-icon:disabled{cursor:default;opacity:.5}.oa-feedback-icon--negative:disabled,.oa-feedback-icon--positive:disabled{opacity:1}.oa-feedback-reason-btn{align-items:center;background:#fef2f2;border:1px solid #fecaca;border-radius:16px;color:#991b1b;cursor:pointer;display:inline-flex;font-family:inherit;font-size:12px;font-weight:500;line-height:18px;padding:4px 12px;transition:background .15s,border-color .15s}.oa-feedback-reason-btn:hover:not(:disabled){background:#fee2e2;border-color:#f87171}.oa-feedback-reason-btn--selected{background:#dc2626!important;border-color:#dc2626!important;color:#fff!important}.oa-feedback-reason-btn:disabled{cursor:default;opacity:.45}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-stream-overlay{display:flex;flex:1;flex-direction:column;min-height:0}.oa-stream-banner{align-items:flex-start;background:#fef3c7;border:1px solid #fde68a;border-radius:8px;color:#92400e;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-stream-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-stream-banner-text{flex:1}.oa-stream-empty{flex:1;gap:16px;justify-content:center;min-height:0}.oa-stream-empty,.oa-stream-empty-text{align-items:center;display:flex;flex-direction:column}.oa-stream-empty-text{gap:6px;text-align:center}.oa-stream-empty-title{color:#111827;font-size:16px;font-weight:700;line-height:20px}.oa-stream-empty-subtitle{color:#6b7280;font-size:14px;line-height:20px}.oa-throttle-banner{align-items:flex-start;background:#fcefee;border:1px solid #e8cecb;border-radius:8px;color:#833d09;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-throttle-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-throttle-banner-text{flex:1}.oa-starting-loader{align-items:center;display:flex;flex:1;flex-direction:column;gap:16px;height:100%;justify-content:center;min-height:0}.oa-starting-spinner{animation:oa-spinner-rotate 1.4s linear infinite;height:48px;width:48px}.oa-starting-spinner-track{stroke:#e5e7eb}.oa-starting-spinner-arc{stroke:#2563eb;stroke-dasharray:80,126;stroke-dashoffset:0;animation:oa-spinner-dash 1.4s ease-in-out infinite}@keyframes oa-spinner-rotate{to{transform:rotate(1turn)}}@keyframes oa-spinner-dash{0%{stroke-dasharray:1,126;stroke-dashoffset:0}50%{stroke-dasharray:80,126;stroke-dashoffset:-35}to{stroke-dasharray:80,126;stroke-dashoffset:-124}}.oa-starting-loader-text{color:#111827;font-size:16px;font-weight:600;line-height:20px}.oa-muted{color:#6b7280}";
21336
21401
  styleInject(css_248z);
21337
21402
 
21338
21403
  function Sparkle() {
@@ -21680,9 +21745,17 @@ const EmptyState = ({
21680
21745
  function SuggestionsTab({
21681
21746
  suggestions,
21682
21747
  connected,
21683
- botConfig
21748
+ botConfig,
21749
+ sendSuggestionFeedback
21684
21750
  }) {
21751
+ var _a, _b, _c;
21685
21752
  const toast = useToast();
21753
+ const [expandedSequence, setExpandedSequence] = reactExports.useState(null);
21754
+ const feedbackEnabled = ((_a = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _a === void 0 ? void 0 : _a.feedback_enabled) === true;
21755
+ const badFeedbackOptions = reactExports.useMemo(() => {
21756
+ var _a, _b;
21757
+ return (_b = (_a = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _a === void 0 ? void 0 : _a.bad_feedback_options) !== null && _b !== void 0 ? _b : [];
21758
+ }, [(_b = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _b === void 0 ? void 0 : _b.bad_feedback_options]);
21686
21759
  const handleCopy = async text => {
21687
21760
  try {
21688
21761
  await navigator.clipboard.writeText(text);
@@ -21691,13 +21764,36 @@ function SuggestionsTab({
21691
21764
  toast.error("Failed to copy");
21692
21765
  }
21693
21766
  };
21767
+ const handleThumbsUp = reactExports.useCallback((sequence, currentFeedback) => {
21768
+ if (!feedbackEnabled) return;
21769
+ const newType = currentFeedback === "good" ? null : "good";
21770
+ sendSuggestionFeedback(sequence, newType, null);
21771
+ setExpandedSequence(null);
21772
+ }, [sendSuggestionFeedback, feedbackEnabled]);
21773
+ const handleThumbsDown = reactExports.useCallback((sequence, currentFeedback) => {
21774
+ if (!feedbackEnabled) return;
21775
+ if (currentFeedback === "bad") {
21776
+ sendSuggestionFeedback(sequence, null, null);
21777
+ setExpandedSequence(null);
21778
+ return;
21779
+ }
21780
+ sendSuggestionFeedback(sequence, "bad", null);
21781
+ if (badFeedbackOptions.length > 0) {
21782
+ setExpandedSequence(sequence);
21783
+ }
21784
+ }, [sendSuggestionFeedback, badFeedbackOptions, feedbackEnabled]);
21785
+ const handleBadFeedbackReason = reactExports.useCallback((sequence, reason) => {
21786
+ if (!feedbackEnabled) return;
21787
+ sendSuggestionFeedback(sequence, "bad", reason);
21788
+ setExpandedSequence(null);
21789
+ }, [sendSuggestionFeedback, feedbackEnabled]);
21694
21790
  if (!connected) {
21695
21791
  return jsxRuntimeExports.jsx(EmptyState, {
21696
21792
  title: "Assistant is inactive",
21697
21793
  subtitle: "Start a call to receive real-time suggestions."
21698
21794
  });
21699
21795
  }
21700
- if ((botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestions) === false) {
21796
+ if (((_c = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _c === void 0 ? void 0 : _c.enabled) === false) {
21701
21797
  return jsxRuntimeExports.jsx(EmptyState, {
21702
21798
  title: "Suggestions are disabled",
21703
21799
  subtitle: "Suggestions are disabled in the bot configuration. Please contact the administrator to enable it."
@@ -21731,21 +21827,37 @@ function SuggestionsTab({
21731
21827
  children: displayed.map((suggestion, index) => {
21732
21828
  const isRecent = index === 0;
21733
21829
  const cardClass = isRecent ? "oa-suggestion-card oa-suggestion-card--recent" : "oa-suggestion-card oa-suggestion-card--older";
21830
+ const showReasons = suggestion.feedbackType === "bad" || expandedSequence === suggestion.sequence;
21734
21831
  return jsxRuntimeExports.jsxs("div", {
21735
- style: {
21736
- display: "flex",
21737
- flexDirection: "row",
21738
- alignItems: "flex-start",
21739
- gap: "8px"
21740
- },
21741
- children: [isRecent ? jsxRuntimeExports.jsx("div", {
21742
- className: "oa-suggestion-card--recent-wrapper",
21832
+ children: [jsxRuntimeExports.jsxs("div", {
21743
21833
  style: {
21744
- flex: "1 1 auto",
21745
- maxWidth: "80%"
21834
+ display: "flex",
21835
+ flexDirection: "row",
21836
+ alignItems: "flex-start",
21837
+ gap: "8px"
21746
21838
  },
21747
- children: jsxRuntimeExports.jsx("div", {
21839
+ children: [isRecent ? jsxRuntimeExports.jsx("div", {
21840
+ className: "oa-suggestion-card--recent-wrapper",
21841
+ style: {
21842
+ flex: "1 1 auto",
21843
+ maxWidth: "72%"
21844
+ },
21845
+ children: jsxRuntimeExports.jsx("div", {
21846
+ className: cardClass,
21847
+ children: jsxRuntimeExports.jsx("span", {
21848
+ className: "oa-suggestion-text",
21849
+ style: {
21850
+ fontSize: "15px"
21851
+ },
21852
+ children: suggestion.text
21853
+ })
21854
+ })
21855
+ }) : jsxRuntimeExports.jsx("div", {
21748
21856
  className: cardClass,
21857
+ style: {
21858
+ flex: "1 1 auto",
21859
+ maxWidth: "72%"
21860
+ },
21749
21861
  children: jsxRuntimeExports.jsx("span", {
21750
21862
  className: "oa-suggestion-text",
21751
21863
  style: {
@@ -21753,31 +21865,57 @@ function SuggestionsTab({
21753
21865
  },
21754
21866
  children: suggestion.value
21755
21867
  })
21756
- })
21757
- }) : jsxRuntimeExports.jsx("div", {
21758
- className: cardClass,
21759
- style: {
21760
- flex: "1 1 auto",
21761
- maxWidth: "80%"
21762
- },
21763
- children: jsxRuntimeExports.jsx("span", {
21764
- className: "oa-suggestion-text",
21868
+ }), jsxRuntimeExports.jsxs("div", {
21765
21869
  style: {
21766
- fontSize: "15px"
21870
+ display: "flex",
21871
+ flexDirection: "column",
21872
+ gap: "4px",
21873
+ flexShrink: 0,
21874
+ marginTop: "10px"
21767
21875
  },
21768
- children: suggestion.value
21769
- })
21770
- }), jsxRuntimeExports.jsx("button", {
21771
- className: "oa-copy-icon",
21772
- "aria-label": "Copy suggestion",
21773
- onClick: () => handleCopy(suggestion.value),
21876
+ children: [jsxRuntimeExports.jsx("button", {
21877
+ className: "oa-copy-icon",
21878
+ "aria-label": "Copy suggestion",
21879
+ onClick: () => handleCopy(suggestion.text),
21880
+ children: jsxRuntimeExports.jsx(Copy, {
21881
+ size: 14
21882
+ })
21883
+ }), feedbackEnabled && jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
21884
+ children: [jsxRuntimeExports.jsx("button", {
21885
+ className: `oa-feedback-icon ${suggestion.feedbackType === "good" ? "oa-feedback-icon--positive" : ""}`,
21886
+ "aria-label": "Mark as helpful",
21887
+ onClick: () => handleThumbsUp(suggestion.sequence, suggestion.feedbackType),
21888
+ style: {
21889
+ color: suggestion.feedbackType === "good" ? "#16a34a" : undefined
21890
+ },
21891
+ children: jsxRuntimeExports.jsx(ThumbsUp, {
21892
+ size: 14
21893
+ })
21894
+ }), jsxRuntimeExports.jsx("button", {
21895
+ className: `oa-feedback-icon ${suggestion.feedbackType === "bad" ? "oa-feedback-icon--negative" : ""}`,
21896
+ "aria-label": "Mark as not helpful",
21897
+ onClick: () => handleThumbsDown(suggestion.sequence, suggestion.feedbackType),
21898
+ style: {
21899
+ color: suggestion.feedbackType === "bad" ? "#dc2626" : undefined
21900
+ },
21901
+ children: jsxRuntimeExports.jsx(ThumbsDown, {
21902
+ size: 14
21903
+ })
21904
+ })]
21905
+ })]
21906
+ })]
21907
+ }), showReasons && badFeedbackOptions.length > 0 && jsxRuntimeExports.jsx("div", {
21774
21908
  style: {
21775
- flexShrink: 0,
21776
- marginTop: "10px"
21909
+ display: "flex",
21910
+ gap: "8px",
21911
+ flexWrap: "wrap",
21912
+ marginTop: "8px"
21777
21913
  },
21778
- children: jsxRuntimeExports.jsx(Copy, {
21779
- size: 14
21780
- })
21914
+ children: badFeedbackOptions.map(reason => jsxRuntimeExports.jsx("button", {
21915
+ className: `oa-feedback-reason-btn ${suggestion.badFeedbackReason === reason ? "oa-feedback-reason-btn--selected" : ""}`,
21916
+ onClick: () => handleBadFeedbackReason(suggestion.sequence, reason),
21917
+ children: reason
21918
+ }, reason))
21781
21919
  })]
21782
21920
  }, suggestion.id);
21783
21921
  })
@@ -21876,12 +22014,13 @@ function ExotelAIAssist(_a) {
21876
22014
  } = _a,
21877
22015
  params = __rest(_a, ["className", "onReady"]);
21878
22016
  const {
21879
- isReady,
21880
22017
  streamState,
22018
+ isReady,
21881
22019
  suggestions,
21882
22020
  transcripts,
21883
22021
  sentiment,
21884
- botConfig
22022
+ botConfig,
22023
+ sendSuggestionFeedback
21885
22024
  } = useExotelAIAssist(params);
21886
22025
  const onReadyRef = reactExports.useRef(onReady);
21887
22026
  onReadyRef.current = onReady;
@@ -21959,7 +22098,8 @@ function ExotelAIAssist(_a) {
21959
22098
  children: jsxRuntimeExports.jsx(SuggestionsTab, {
21960
22099
  suggestions: suggestions,
21961
22100
  connected: connected,
21962
- botConfig: botConfig
22101
+ botConfig: botConfig,
22102
+ sendSuggestionFeedback: sendSuggestionFeedback
21963
22103
  })
21964
22104
  }), jsxRuntimeExports.jsx(Content$1, {
21965
22105
  value: "transcript",
@@ -4576,6 +4576,30 @@ const Smile = createLucideIcon$1("Smile", [["circle", {
4576
4576
  key: "1p4y9e"
4577
4577
  }]]);
4578
4578
 
4579
+ /**
4580
+ * lucide-react v0.0.1 - ISC
4581
+ */
4582
+
4583
+ const ThumbsDown = createLucideIcon$1("ThumbsDown", [["path", {
4584
+ d: "M17 14V2",
4585
+ key: "8ymqnk"
4586
+ }], ["path", {
4587
+ d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z",
4588
+ key: "s6e0r"
4589
+ }]]);
4590
+
4591
+ /**
4592
+ * lucide-react v0.0.1 - ISC
4593
+ */
4594
+
4595
+ const ThumbsUp = createLucideIcon$1("ThumbsUp", [["path", {
4596
+ d: "M7 10v12",
4597
+ key: "1qc93n"
4598
+ }], ["path", {
4599
+ d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z",
4600
+ key: "y3tblf"
4601
+ }]]);
4602
+
4579
4603
  const MAX_VISIBLE_TOASTS = 5;
4580
4604
  const AUTO_DISMISS_MS = 3000;
4581
4605
  const ToastContext = React.createContext(null);
@@ -6371,6 +6395,25 @@ class ExotelAIAssistController extends EventEmitter {
6371
6395
  getStreamState() {
6372
6396
  return this._streamState;
6373
6397
  }
6398
+ sendSuggestionFeedback(sequence, feedbackType, badFeedbackReason = null) {
6399
+ if (!this.transport || this.status !== "connected") {
6400
+ console.warn("[ExotelAIAssist] Cannot send feedback: not connected");
6401
+ return false;
6402
+ }
6403
+ const message = {
6404
+ type: "suggestion_feedback",
6405
+ sequence,
6406
+ feedback_type: feedbackType,
6407
+ bad_feedback_reason: badFeedbackReason
6408
+ };
6409
+ try {
6410
+ this.transport.send(JSON.stringify(message));
6411
+ return true;
6412
+ } catch (error) {
6413
+ console.error("[ExotelAIAssist] Failed to send feedback:", error);
6414
+ return false;
6415
+ }
6416
+ }
6374
6417
  _ensureTransport() {
6375
6418
  if (this.transport) return;
6376
6419
  this.transport = createTransport(Utils.hash(this.params));
@@ -6493,17 +6536,27 @@ class ExotelAIAssistController extends EventEmitter {
6493
6536
  });
6494
6537
  this.emit("transcript", lines);
6495
6538
  }
6496
- if (event.event_type === "suggestion" && event.value) {
6497
- const suggestion = {
6498
- id: Utils.getUniqueId(),
6499
- value: event.value,
6500
- timestamp: now
6501
- };
6502
- this.emit("suggestion", suggestion);
6539
+ if (event.event_type === "suggestion") {
6540
+ const val = event.value;
6541
+ const text = typeof val === "object" && val !== null && "text" in val ? val.text : typeof val === "string" ? val : event.text;
6542
+ const sequence = typeof val === "object" && val !== null && "sequence" in val ? val.sequence : 0;
6543
+ if (text) {
6544
+ const suggestion = {
6545
+ id: Utils.getUniqueId(),
6546
+ text,
6547
+ value: text,
6548
+ timestamp: now,
6549
+ sequence,
6550
+ feedbackType: null,
6551
+ badFeedbackReason: null
6552
+ };
6553
+ this.emit("suggestion", suggestion);
6554
+ }
6503
6555
  }
6504
6556
  if (event.event_type === "sentiment" && event.value) {
6557
+ const sentimentValue = typeof event.value === "string" ? event.value : event.value.text;
6505
6558
  const sentiment = {
6506
- label: event.value.toLowerCase(),
6559
+ label: sentimentValue.toLowerCase(),
6507
6560
  timestamp: now
6508
6561
  };
6509
6562
  this.emit("sentiment", sentiment);
@@ -6579,9 +6632,20 @@ function useExotelAIAssist(params) {
6579
6632
  var _a;
6580
6633
  return (_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.setParams(patch);
6581
6634
  }, []);
6635
+ const sendSuggestionFeedback = React.useCallback((sequence, feedbackType, badFeedbackReason = null) => {
6636
+ var _a, _b;
6637
+ const sent = (_b = (_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.sendSuggestionFeedback(sequence, feedbackType, badFeedbackReason)) !== null && _b !== void 0 ? _b : false;
6638
+ if (sent) {
6639
+ setSuggestions(prev => prev.map(s => s.sequence === sequence ? Object.assign(Object.assign({}, s), {
6640
+ feedbackType,
6641
+ badFeedbackReason
6642
+ }) : s));
6643
+ }
6644
+ return sent;
6645
+ }, []);
6582
6646
  return {
6583
- isReady,
6584
6647
  streamState,
6648
+ isReady,
6585
6649
  suggestions,
6586
6650
  transcripts,
6587
6651
  sentiment,
@@ -6589,7 +6653,8 @@ function useExotelAIAssist(params) {
6589
6653
  lastError,
6590
6654
  connect,
6591
6655
  disconnect,
6592
- setParams
6656
+ setParams,
6657
+ sendSuggestionFeedback
6593
6658
  };
6594
6659
  }
6595
6660
 
@@ -6618,7 +6683,7 @@ function styleInject(css, ref) {
6618
6683
  }
6619
6684
  }
6620
6685
 
6621
- var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{flex:1;min-height:0;overflow:hidden}.oa-tabs-content[data-state=inactive]{display:none!important}.oa-tabs-content[data-state=active]{display:flex;flex-direction:column}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px;text-align:left}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px;text-align:left}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover{background:#f9fafb;color:#374151}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-stream-overlay{display:flex;flex:1;flex-direction:column;min-height:0}.oa-stream-banner{align-items:flex-start;background:#fef3c7;border:1px solid #fde68a;border-radius:8px;color:#92400e;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-stream-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-stream-banner-text{flex:1}.oa-stream-empty{flex:1;gap:16px;justify-content:center;min-height:0}.oa-stream-empty,.oa-stream-empty-text{align-items:center;display:flex;flex-direction:column}.oa-stream-empty-text{gap:6px;text-align:center}.oa-stream-empty-title{color:#111827;font-size:16px;font-weight:700;line-height:20px}.oa-stream-empty-subtitle{color:#6b7280;font-size:14px;line-height:20px}.oa-throttle-banner{align-items:flex-start;background:#fcefee;border:1px solid #e8cecb;border-radius:8px;color:#833d09;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-throttle-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-throttle-banner-text{flex:1}.oa-starting-loader{align-items:center;display:flex;flex:1;flex-direction:column;gap:16px;height:100%;justify-content:center;min-height:0}.oa-starting-spinner{animation:oa-spinner-rotate 1.4s linear infinite;height:48px;width:48px}.oa-starting-spinner-track{stroke:#e5e7eb}.oa-starting-spinner-arc{stroke:#2563eb;stroke-dasharray:80,126;stroke-dashoffset:0;animation:oa-spinner-dash 1.4s ease-in-out infinite}@keyframes oa-spinner-rotate{to{transform:rotate(1turn)}}@keyframes oa-spinner-dash{0%{stroke-dasharray:1,126;stroke-dashoffset:0}50%{stroke-dasharray:80,126;stroke-dashoffset:-35}to{stroke-dasharray:80,126;stroke-dashoffset:-124}}.oa-starting-loader-text{color:#111827;font-size:16px;font-weight:600;line-height:20px}.oa-muted{color:#6b7280}";
6686
+ var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{flex:1;min-height:0;overflow:hidden}.oa-tabs-content[data-state=inactive]{display:none!important}.oa-tabs-content[data-state=active]{display:flex;flex-direction:column}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px;text-align:left}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px;text-align:left}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon,.oa-feedback-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover,.oa-feedback-icon:hover{background:#f9fafb;color:#374151}.oa-feedback-icon--positive{background:#dcfce7!important;border-color:#86efac!important;color:#16a34a!important}.oa-feedback-icon--negative{background:#fee2e2!important;border-color:#fca5a5!important;color:#dc2626!important}.oa-feedback-icon:disabled{cursor:default;opacity:.5}.oa-feedback-icon--negative:disabled,.oa-feedback-icon--positive:disabled{opacity:1}.oa-feedback-reason-btn{align-items:center;background:#fef2f2;border:1px solid #fecaca;border-radius:16px;color:#991b1b;cursor:pointer;display:inline-flex;font-family:inherit;font-size:12px;font-weight:500;line-height:18px;padding:4px 12px;transition:background .15s,border-color .15s}.oa-feedback-reason-btn:hover:not(:disabled){background:#fee2e2;border-color:#f87171}.oa-feedback-reason-btn--selected{background:#dc2626!important;border-color:#dc2626!important;color:#fff!important}.oa-feedback-reason-btn:disabled{cursor:default;opacity:.45}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-stream-overlay{display:flex;flex:1;flex-direction:column;min-height:0}.oa-stream-banner{align-items:flex-start;background:#fef3c7;border:1px solid #fde68a;border-radius:8px;color:#92400e;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-stream-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-stream-banner-text{flex:1}.oa-stream-empty{flex:1;gap:16px;justify-content:center;min-height:0}.oa-stream-empty,.oa-stream-empty-text{align-items:center;display:flex;flex-direction:column}.oa-stream-empty-text{gap:6px;text-align:center}.oa-stream-empty-title{color:#111827;font-size:16px;font-weight:700;line-height:20px}.oa-stream-empty-subtitle{color:#6b7280;font-size:14px;line-height:20px}.oa-throttle-banner{align-items:flex-start;background:#fcefee;border:1px solid #e8cecb;border-radius:8px;color:#833d09;display:flex;font-size:13px;gap:8px;line-height:1.4;padding:10px 12px}.oa-throttle-banner-icon{align-items:center;display:inline-flex;flex-shrink:0;margin-top:1px}.oa-throttle-banner-text{flex:1}.oa-starting-loader{align-items:center;display:flex;flex:1;flex-direction:column;gap:16px;height:100%;justify-content:center;min-height:0}.oa-starting-spinner{animation:oa-spinner-rotate 1.4s linear infinite;height:48px;width:48px}.oa-starting-spinner-track{stroke:#e5e7eb}.oa-starting-spinner-arc{stroke:#2563eb;stroke-dasharray:80,126;stroke-dashoffset:0;animation:oa-spinner-dash 1.4s ease-in-out infinite}@keyframes oa-spinner-rotate{to{transform:rotate(1turn)}}@keyframes oa-spinner-dash{0%{stroke-dasharray:1,126;stroke-dashoffset:0}50%{stroke-dasharray:80,126;stroke-dashoffset:-35}to{stroke-dasharray:80,126;stroke-dashoffset:-124}}.oa-starting-loader-text{color:#111827;font-size:16px;font-weight:600;line-height:20px}.oa-muted{color:#6b7280}";
6622
6687
  styleInject(css_248z);
6623
6688
 
6624
6689
  function Sparkle() {
@@ -6966,9 +7031,17 @@ const EmptyState = ({
6966
7031
  function SuggestionsTab({
6967
7032
  suggestions,
6968
7033
  connected,
6969
- botConfig
7034
+ botConfig,
7035
+ sendSuggestionFeedback
6970
7036
  }) {
7037
+ var _a, _b, _c;
6971
7038
  const toast = useToast();
7039
+ const [expandedSequence, setExpandedSequence] = React.useState(null);
7040
+ const feedbackEnabled = ((_a = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _a === void 0 ? void 0 : _a.feedback_enabled) === true;
7041
+ const badFeedbackOptions = React.useMemo(() => {
7042
+ var _a, _b;
7043
+ return (_b = (_a = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _a === void 0 ? void 0 : _a.bad_feedback_options) !== null && _b !== void 0 ? _b : [];
7044
+ }, [(_b = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _b === void 0 ? void 0 : _b.bad_feedback_options]);
6972
7045
  const handleCopy = async text => {
6973
7046
  try {
6974
7047
  await navigator.clipboard.writeText(text);
@@ -6977,13 +7050,36 @@ function SuggestionsTab({
6977
7050
  toast.error("Failed to copy");
6978
7051
  }
6979
7052
  };
7053
+ const handleThumbsUp = React.useCallback((sequence, currentFeedback) => {
7054
+ if (!feedbackEnabled) return;
7055
+ const newType = currentFeedback === "good" ? null : "good";
7056
+ sendSuggestionFeedback(sequence, newType, null);
7057
+ setExpandedSequence(null);
7058
+ }, [sendSuggestionFeedback, feedbackEnabled]);
7059
+ const handleThumbsDown = React.useCallback((sequence, currentFeedback) => {
7060
+ if (!feedbackEnabled) return;
7061
+ if (currentFeedback === "bad") {
7062
+ sendSuggestionFeedback(sequence, null, null);
7063
+ setExpandedSequence(null);
7064
+ return;
7065
+ }
7066
+ sendSuggestionFeedback(sequence, "bad", null);
7067
+ if (badFeedbackOptions.length > 0) {
7068
+ setExpandedSequence(sequence);
7069
+ }
7070
+ }, [sendSuggestionFeedback, badFeedbackOptions, feedbackEnabled]);
7071
+ const handleBadFeedbackReason = React.useCallback((sequence, reason) => {
7072
+ if (!feedbackEnabled) return;
7073
+ sendSuggestionFeedback(sequence, "bad", reason);
7074
+ setExpandedSequence(null);
7075
+ }, [sendSuggestionFeedback, feedbackEnabled]);
6980
7076
  if (!connected) {
6981
7077
  return jsxRuntime.jsx(EmptyState, {
6982
7078
  title: "Assistant is inactive",
6983
7079
  subtitle: "Start a call to receive real-time suggestions."
6984
7080
  });
6985
7081
  }
6986
- if ((botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestions) === false) {
7082
+ if (((_c = botConfig === null || botConfig === void 0 ? void 0 : botConfig.suggestion) === null || _c === void 0 ? void 0 : _c.enabled) === false) {
6987
7083
  return jsxRuntime.jsx(EmptyState, {
6988
7084
  title: "Suggestions are disabled",
6989
7085
  subtitle: "Suggestions are disabled in the bot configuration. Please contact the administrator to enable it."
@@ -7017,21 +7113,37 @@ function SuggestionsTab({
7017
7113
  children: displayed.map((suggestion, index) => {
7018
7114
  const isRecent = index === 0;
7019
7115
  const cardClass = isRecent ? "oa-suggestion-card oa-suggestion-card--recent" : "oa-suggestion-card oa-suggestion-card--older";
7116
+ const showReasons = suggestion.feedbackType === "bad" || expandedSequence === suggestion.sequence;
7020
7117
  return jsxRuntime.jsxs("div", {
7021
- style: {
7022
- display: "flex",
7023
- flexDirection: "row",
7024
- alignItems: "flex-start",
7025
- gap: "8px"
7026
- },
7027
- children: [isRecent ? jsxRuntime.jsx("div", {
7028
- className: "oa-suggestion-card--recent-wrapper",
7118
+ children: [jsxRuntime.jsxs("div", {
7029
7119
  style: {
7030
- flex: "1 1 auto",
7031
- maxWidth: "80%"
7120
+ display: "flex",
7121
+ flexDirection: "row",
7122
+ alignItems: "flex-start",
7123
+ gap: "8px"
7032
7124
  },
7033
- children: jsxRuntime.jsx("div", {
7125
+ children: [isRecent ? jsxRuntime.jsx("div", {
7126
+ className: "oa-suggestion-card--recent-wrapper",
7127
+ style: {
7128
+ flex: "1 1 auto",
7129
+ maxWidth: "72%"
7130
+ },
7131
+ children: jsxRuntime.jsx("div", {
7132
+ className: cardClass,
7133
+ children: jsxRuntime.jsx("span", {
7134
+ className: "oa-suggestion-text",
7135
+ style: {
7136
+ fontSize: "15px"
7137
+ },
7138
+ children: suggestion.text
7139
+ })
7140
+ })
7141
+ }) : jsxRuntime.jsx("div", {
7034
7142
  className: cardClass,
7143
+ style: {
7144
+ flex: "1 1 auto",
7145
+ maxWidth: "72%"
7146
+ },
7035
7147
  children: jsxRuntime.jsx("span", {
7036
7148
  className: "oa-suggestion-text",
7037
7149
  style: {
@@ -7039,31 +7151,57 @@ function SuggestionsTab({
7039
7151
  },
7040
7152
  children: suggestion.value
7041
7153
  })
7042
- })
7043
- }) : jsxRuntime.jsx("div", {
7044
- className: cardClass,
7045
- style: {
7046
- flex: "1 1 auto",
7047
- maxWidth: "80%"
7048
- },
7049
- children: jsxRuntime.jsx("span", {
7050
- className: "oa-suggestion-text",
7154
+ }), jsxRuntime.jsxs("div", {
7051
7155
  style: {
7052
- fontSize: "15px"
7156
+ display: "flex",
7157
+ flexDirection: "column",
7158
+ gap: "4px",
7159
+ flexShrink: 0,
7160
+ marginTop: "10px"
7053
7161
  },
7054
- children: suggestion.value
7055
- })
7056
- }), jsxRuntime.jsx("button", {
7057
- className: "oa-copy-icon",
7058
- "aria-label": "Copy suggestion",
7059
- onClick: () => handleCopy(suggestion.value),
7162
+ children: [jsxRuntime.jsx("button", {
7163
+ className: "oa-copy-icon",
7164
+ "aria-label": "Copy suggestion",
7165
+ onClick: () => handleCopy(suggestion.text),
7166
+ children: jsxRuntime.jsx(Copy, {
7167
+ size: 14
7168
+ })
7169
+ }), feedbackEnabled && jsxRuntime.jsxs(jsxRuntime.Fragment, {
7170
+ children: [jsxRuntime.jsx("button", {
7171
+ className: `oa-feedback-icon ${suggestion.feedbackType === "good" ? "oa-feedback-icon--positive" : ""}`,
7172
+ "aria-label": "Mark as helpful",
7173
+ onClick: () => handleThumbsUp(suggestion.sequence, suggestion.feedbackType),
7174
+ style: {
7175
+ color: suggestion.feedbackType === "good" ? "#16a34a" : undefined
7176
+ },
7177
+ children: jsxRuntime.jsx(ThumbsUp, {
7178
+ size: 14
7179
+ })
7180
+ }), jsxRuntime.jsx("button", {
7181
+ className: `oa-feedback-icon ${suggestion.feedbackType === "bad" ? "oa-feedback-icon--negative" : ""}`,
7182
+ "aria-label": "Mark as not helpful",
7183
+ onClick: () => handleThumbsDown(suggestion.sequence, suggestion.feedbackType),
7184
+ style: {
7185
+ color: suggestion.feedbackType === "bad" ? "#dc2626" : undefined
7186
+ },
7187
+ children: jsxRuntime.jsx(ThumbsDown, {
7188
+ size: 14
7189
+ })
7190
+ })]
7191
+ })]
7192
+ })]
7193
+ }), showReasons && badFeedbackOptions.length > 0 && jsxRuntime.jsx("div", {
7060
7194
  style: {
7061
- flexShrink: 0,
7062
- marginTop: "10px"
7195
+ display: "flex",
7196
+ gap: "8px",
7197
+ flexWrap: "wrap",
7198
+ marginTop: "8px"
7063
7199
  },
7064
- children: jsxRuntime.jsx(Copy, {
7065
- size: 14
7066
- })
7200
+ children: badFeedbackOptions.map(reason => jsxRuntime.jsx("button", {
7201
+ className: `oa-feedback-reason-btn ${suggestion.badFeedbackReason === reason ? "oa-feedback-reason-btn--selected" : ""}`,
7202
+ onClick: () => handleBadFeedbackReason(suggestion.sequence, reason),
7203
+ children: reason
7204
+ }, reason))
7067
7205
  })]
7068
7206
  }, suggestion.id);
7069
7207
  })
@@ -7162,12 +7300,13 @@ function ExotelAIAssist(_a) {
7162
7300
  } = _a,
7163
7301
  params = __rest(_a, ["className", "onReady"]);
7164
7302
  const {
7165
- isReady,
7166
7303
  streamState,
7304
+ isReady,
7167
7305
  suggestions,
7168
7306
  transcripts,
7169
7307
  sentiment,
7170
- botConfig
7308
+ botConfig,
7309
+ sendSuggestionFeedback
7171
7310
  } = useExotelAIAssist(params);
7172
7311
  const onReadyRef = React.useRef(onReady);
7173
7312
  onReadyRef.current = onReady;
@@ -7245,7 +7384,8 @@ function ExotelAIAssist(_a) {
7245
7384
  children: jsxRuntime.jsx(SuggestionsTab, {
7246
7385
  suggestions: suggestions,
7247
7386
  connected: connected,
7248
- botConfig: botConfig
7387
+ botConfig: botConfig,
7388
+ sendSuggestionFeedback: sendSuggestionFeedback
7249
7389
  })
7250
7390
  }), jsxRuntime.jsx(Content$1, {
7251
7391
  value: "transcript",
@@ -12,8 +12,12 @@ export interface ExotelAIAssistParams {
12
12
  /** A single AI-generated suggestion for the agent. */
13
13
  export interface Suggestion {
14
14
  id: string;
15
+ text: string;
15
16
  value: string;
16
17
  timestamp: number;
18
+ sequence: number;
19
+ feedbackType: "good" | "bad" | null;
20
+ badFeedbackReason: string | null;
17
21
  }
18
22
  /** A single spoken line as received in the live transcript. */
19
23
  export interface TranscriptLine {
@@ -43,7 +47,11 @@ export type StreamState = "connected" | "throttled" | "pending" | "disconnected"
43
47
  export interface BotConfig {
44
48
  sentiment: boolean;
45
49
  transcript: boolean;
46
- suggestions: boolean;
50
+ suggestion: {
51
+ enabled: boolean;
52
+ feedback_enabled: boolean;
53
+ bad_feedback_options?: string[];
54
+ };
47
55
  status: "LIVE" | "DRAFT" | "DEACTIVATED";
48
56
  }
49
57
  interface TranscriptSegment {
@@ -56,10 +64,21 @@ interface TranscriptMessage {
56
64
  sequence: number;
57
65
  transcript_segments: TranscriptSegment[];
58
66
  }
67
+ export interface SuggestionValue {
68
+ text: string;
69
+ sequence: number;
70
+ }
59
71
  export interface WssEvent {
60
72
  event_type: "suggestion" | "sentiment" | "transcript";
61
73
  transcript: TranscriptMessage[];
62
- value: string;
74
+ text: string;
75
+ value?: SuggestionValue | string;
76
+ }
77
+ export interface SuggestionFeedbackMessage {
78
+ type: "suggestion_feedback";
79
+ sequence: number;
80
+ feedback_type: "good" | "bad" | null;
81
+ bad_feedback_reason: string | null;
63
82
  }
64
83
  export interface InitialHandshakeResponse {
65
84
  type: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exotel-npm-dev/exotel-ai-assist",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "AI-powered real-time suggestions, transcript, and sentiment for agent calls",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,7 @@ export interface ExotelAIAssistProps extends ExotelAIAssistParams {
19
19
  }
20
20
 
21
21
  export function ExotelAIAssist({ className, onReady, ...params }: ExotelAIAssistProps): JSX.Element {
22
- const { isReady, streamState, suggestions, transcripts, sentiment, botConfig } = useExotelAIAssist(params as ExotelAIAssistParams);
22
+ const { streamState, isReady, suggestions, transcripts, sentiment, botConfig, sendSuggestionFeedback } = useExotelAIAssist(params as ExotelAIAssistParams);
23
23
 
24
24
  const onReadyRef = useRef(onReady);
25
25
  onReadyRef.current = onReady;
@@ -68,9 +68,9 @@ export function ExotelAIAssist({ className, onReady, ...params }: ExotelAIAssist
68
68
  </Tabs.Trigger>
69
69
  </Tabs.List>
70
70
 
71
- <Tabs.Content value="suggestions" className="oa-tabs-content" style={{ paddingTop: 16 }}>
72
- <SuggestionsTab suggestions={suggestions} connected={connected} botConfig={botConfig} />
73
- </Tabs.Content>
71
+ <Tabs.Content value="suggestions" className="oa-tabs-content" style={{ paddingTop: 16 }}>
72
+ <SuggestionsTab suggestions={suggestions} connected={connected} botConfig={botConfig} sendSuggestionFeedback={sendSuggestionFeedback} />
73
+ </Tabs.Content>
74
74
 
75
75
  <Tabs.Content value="transcript" className="oa-tabs-content" style={{ paddingTop: 16 }}>
76
76
  <TranscriptTab transcripts={transcripts} connected={connected} botConfig={botConfig} />
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { Copy } from "lucide-react";
1
+ import React, { useState, useCallback, useMemo } from "react";
2
+ import { Copy, ThumbsUp, ThumbsDown } from "lucide-react";
3
3
 
4
4
  import { Suggestion, BotConfig } from "../../types";
5
5
  import LoadingBox from "../LoadingBox";
@@ -11,12 +11,19 @@ export function SuggestionsTab({
11
11
  suggestions,
12
12
  connected,
13
13
  botConfig,
14
+ sendSuggestionFeedback,
14
15
  }: {
15
16
  suggestions: Suggestion[];
16
17
  connected: boolean;
17
18
  botConfig: BotConfig | null;
19
+ sendSuggestionFeedback: (sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason?: string | null) => boolean;
18
20
  }): JSX.Element {
19
21
  const toast = useToast();
22
+ const [expandedSequence, setExpandedSequence] = useState<number | null>(null);
23
+
24
+ const feedbackEnabled = botConfig?.suggestion?.feedback_enabled === true;
25
+
26
+ const badFeedbackOptions = useMemo(() => botConfig?.suggestion?.bad_feedback_options ?? [], [botConfig?.suggestion?.bad_feedback_options]);
20
27
 
21
28
  const handleCopy = async (text: string) => {
22
29
  try {
@@ -27,11 +34,46 @@ export function SuggestionsTab({
27
34
  }
28
35
  };
29
36
 
37
+ const handleThumbsUp = useCallback(
38
+ (sequence: number, currentFeedback: "good" | "bad" | null) => {
39
+ if (!feedbackEnabled) return;
40
+ const newType = currentFeedback === "good" ? null : "good";
41
+ sendSuggestionFeedback(sequence, newType, null);
42
+ setExpandedSequence(null);
43
+ },
44
+ [sendSuggestionFeedback, feedbackEnabled]
45
+ );
46
+
47
+ const handleThumbsDown = useCallback(
48
+ (sequence: number, currentFeedback: "good" | "bad" | null) => {
49
+ if (!feedbackEnabled) return;
50
+ if (currentFeedback === "bad") {
51
+ sendSuggestionFeedback(sequence, null, null);
52
+ setExpandedSequence(null);
53
+ return;
54
+ }
55
+ sendSuggestionFeedback(sequence, "bad", null);
56
+ if (badFeedbackOptions.length > 0) {
57
+ setExpandedSequence(sequence);
58
+ }
59
+ },
60
+ [sendSuggestionFeedback, badFeedbackOptions, feedbackEnabled]
61
+ );
62
+
63
+ const handleBadFeedbackReason = useCallback(
64
+ (sequence: number, reason: string) => {
65
+ if (!feedbackEnabled) return;
66
+ sendSuggestionFeedback(sequence, "bad", reason);
67
+ setExpandedSequence(null);
68
+ },
69
+ [sendSuggestionFeedback, feedbackEnabled]
70
+ );
71
+
30
72
  if (!connected) {
31
73
  return <EmptyState title="Assistant is inactive" subtitle="Start a call to receive real-time suggestions." />;
32
74
  }
33
75
 
34
- if (botConfig?.suggestions === false) {
76
+ if (botConfig?.suggestion?.enabled === false) {
35
77
  return <EmptyState title="Suggestions are disabled" subtitle="Suggestions are disabled in the bot configuration. Please contact the administrator to enable it." />;
36
78
  }
37
79
 
@@ -63,27 +105,71 @@ export function SuggestionsTab({
63
105
  {displayed.map((suggestion, index) => {
64
106
  const isRecent = index === 0;
65
107
  const cardClass = isRecent ? "oa-suggestion-card oa-suggestion-card--recent" : "oa-suggestion-card oa-suggestion-card--older";
108
+ const showReasons = suggestion.feedbackType === "bad" || expandedSequence === suggestion.sequence;
66
109
 
67
110
  return (
68
- <div key={suggestion.id} style={{ display: "flex", flexDirection: "row", alignItems: "flex-start", gap: "8px" }}>
69
- {isRecent ? (
70
- <div className="oa-suggestion-card--recent-wrapper" style={{ flex: "1 1 auto", maxWidth: "80%" }}>
71
- <div className={cardClass}>
111
+ <div key={suggestion.id}>
112
+ <div style={{ display: "flex", flexDirection: "row", alignItems: "flex-start", gap: "8px" }}>
113
+ {isRecent ? (
114
+ <div className="oa-suggestion-card--recent-wrapper" style={{ flex: "1 1 auto", maxWidth: "72%" }}>
115
+ <div className={cardClass}>
116
+ <span className="oa-suggestion-text" style={{ fontSize: "15px" }}>
117
+ {suggestion.text}
118
+ </span>
119
+ </div>
120
+ </div>
121
+ ) : (
122
+ <div className={cardClass} style={{ flex: "1 1 auto", maxWidth: "72%" }}>
72
123
  <span className="oa-suggestion-text" style={{ fontSize: "15px" }}>
73
124
  {suggestion.value}
74
125
  </span>
75
126
  </div>
127
+ )}
128
+
129
+ <div style={{ display: "flex", flexDirection: "column", gap: "4px", flexShrink: 0, marginTop: "10px" }}>
130
+ <button className="oa-copy-icon" aria-label="Copy suggestion" onClick={() => handleCopy(suggestion.text)}>
131
+ <Copy size={14} />
132
+ </button>
133
+ {feedbackEnabled && (
134
+ <>
135
+ <button
136
+ className={`oa-feedback-icon ${suggestion.feedbackType === "good" ? "oa-feedback-icon--positive" : ""}`}
137
+ aria-label="Mark as helpful"
138
+ onClick={() => handleThumbsUp(suggestion.sequence, suggestion.feedbackType)}
139
+ style={{
140
+ color: suggestion.feedbackType === "good" ? "#16a34a" : undefined,
141
+ }}
142
+ >
143
+ <ThumbsUp size={14} />
144
+ </button>
145
+ <button
146
+ className={`oa-feedback-icon ${suggestion.feedbackType === "bad" ? "oa-feedback-icon--negative" : ""}`}
147
+ aria-label="Mark as not helpful"
148
+ onClick={() => handleThumbsDown(suggestion.sequence, suggestion.feedbackType)}
149
+ style={{
150
+ color: suggestion.feedbackType === "bad" ? "#dc2626" : undefined,
151
+ }}
152
+ >
153
+ <ThumbsDown size={14} />
154
+ </button>
155
+ </>
156
+ )}
76
157
  </div>
77
- ) : (
78
- <div className={cardClass} style={{ flex: "1 1 auto", maxWidth: "80%" }}>
79
- <span className="oa-suggestion-text" style={{ fontSize: "15px" }}>
80
- {suggestion.value}
81
- </span>
158
+ </div>
159
+
160
+ {showReasons && badFeedbackOptions.length > 0 && (
161
+ <div style={{ display: "flex", gap: "8px", flexWrap: "wrap", marginTop: "8px" }}>
162
+ {badFeedbackOptions.map((reason) => (
163
+ <button
164
+ key={reason}
165
+ className={`oa-feedback-reason-btn ${suggestion.badFeedbackReason === reason ? "oa-feedback-reason-btn--selected" : ""}`}
166
+ onClick={() => handleBadFeedbackReason(suggestion.sequence, reason)}
167
+ >
168
+ {reason}
169
+ </button>
170
+ ))}
82
171
  </div>
83
172
  )}
84
- <button className="oa-copy-icon" aria-label="Copy suggestion" onClick={() => handleCopy(suggestion.value)} style={{ flexShrink: 0, marginTop: "10px" }}>
85
- <Copy size={14} />
86
- </button>
87
173
  </div>
88
174
  );
89
175
  })}
@@ -1,17 +1,5 @@
1
1
  import EventEmitter from "eventemitter3";
2
- import {
3
- ExotelAIAssistParams,
4
- ConnectionStatus,
5
- ControllerEvents,
6
- Suggestion,
7
- TranscriptLine,
8
- Sentiment,
9
- StreamState,
10
- WssEvent,
11
- InitialHandshakeResponse,
12
- WssResponse,
13
- WorkerInboundMessage,
14
- } from "../types";
2
+ import { ExotelAIAssistParams, ConnectionStatus, ControllerEvents, Suggestion, TranscriptLine, Sentiment, WssEvent, InitialHandshakeResponse, WssResponse, WorkerInboundMessage, SuggestionFeedbackMessage, StreamState } from "../types";
15
3
  import { ITransport, createTransport } from "../transport";
16
4
  import { Utils } from "../utils";
17
5
 
@@ -99,6 +87,28 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
99
87
  getStreamState(): StreamState | null {
100
88
  return this._streamState;
101
89
  }
90
+
91
+ sendSuggestionFeedback(sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason: string | null = null): boolean {
92
+ if (!this.transport || this.status !== "connected") {
93
+ console.warn("[ExotelAIAssist] Cannot send feedback: not connected");
94
+ return false;
95
+ }
96
+
97
+ const message: SuggestionFeedbackMessage = {
98
+ type: "suggestion_feedback",
99
+ sequence,
100
+ feedback_type: feedbackType,
101
+ bad_feedback_reason: badFeedbackReason,
102
+ };
103
+
104
+ try {
105
+ this.transport.send(JSON.stringify(message));
106
+ return true;
107
+ } catch (error) {
108
+ console.error("[ExotelAIAssist] Failed to send feedback:", error);
109
+ return false;
110
+ }
111
+ }
102
112
 
103
113
  private _ensureTransport(): void {
104
114
  if (this.transport) return;
@@ -240,18 +250,29 @@ export class ExotelAIAssistController extends EventEmitter<ControllerEvents> {
240
250
  this.emit("transcript", lines);
241
251
  }
242
252
 
243
- if (event.event_type === "suggestion" && event.value) {
244
- const suggestion: Suggestion = {
245
- id: Utils.getUniqueId(),
246
- value: event.value,
247
- timestamp: now,
248
- };
249
- this.emit("suggestion", suggestion);
253
+ if (event.event_type === "suggestion") {
254
+ const val = event.value;
255
+ const text = typeof val === "object" && val !== null && "text" in val ? val.text : typeof val === "string" ? val : event.text;
256
+ const sequence = typeof val === "object" && val !== null && "sequence" in val ? val.sequence : 0;
257
+
258
+ if (text) {
259
+ const suggestion: Suggestion = {
260
+ id: Utils.getUniqueId(),
261
+ text,
262
+ value: text,
263
+ timestamp: now,
264
+ sequence,
265
+ feedbackType: null,
266
+ badFeedbackReason: null,
267
+ };
268
+ this.emit("suggestion", suggestion);
269
+ }
250
270
  }
251
271
 
252
272
  if (event.event_type === "sentiment" && event.value) {
273
+ const sentimentValue = typeof event.value === "string" ? event.value : event.value.text;
253
274
  const sentiment: Sentiment = {
254
- label: event.value.toLowerCase() as Sentiment["label"],
275
+ label: sentimentValue.toLowerCase() as Sentiment["label"],
255
276
  timestamp: now,
256
277
  };
257
278
  this.emit("sentiment", sentiment);
@@ -31,6 +31,7 @@ export interface UseExotelAIAssistReturn {
31
31
  connect: () => void;
32
32
  disconnect: () => void;
33
33
  setParams: (patch: Partial<ExotelAIAssistParams>) => void;
34
+ sendSuggestionFeedback: (sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason?: string | null) => boolean;
34
35
  }
35
36
 
36
37
  /** Internal extended return — adds botConfig for the UI component only. */
@@ -89,5 +90,20 @@ export function useExotelAIAssist(params: ExotelAIAssistParams): UseExotelAIAssi
89
90
  const disconnect = useCallback(() => controllerRef.current?.disconnect(), []);
90
91
  const setParams = useCallback((patch: Partial<ExotelAIAssistParams>) => controllerRef.current?.setParams(patch), []);
91
92
 
92
- return { isReady, streamState, suggestions, transcripts, sentiment, botConfig, lastError, connect, disconnect, setParams };
93
+ const sendSuggestionFeedback = useCallback(
94
+ (sequence: number, feedbackType: "good" | "bad" | null, badFeedbackReason: string | null = null): boolean => {
95
+ const sent = controllerRef.current?.sendSuggestionFeedback(sequence, feedbackType, badFeedbackReason) ?? false;
96
+
97
+ if (sent) {
98
+ setSuggestions((prev) =>
99
+ prev.map((s) => (s.sequence === sequence ? { ...s, feedbackType, badFeedbackReason } : s))
100
+ );
101
+ }
102
+
103
+ return sent;
104
+ },
105
+ []
106
+ );
107
+
108
+ return { streamState, isReady, suggestions, transcripts, sentiment, botConfig, lastError, connect, disconnect, setParams, sendSuggestionFeedback };
93
109
  }
@@ -178,9 +178,10 @@
178
178
  }
179
179
 
180
180
  /* -----------------------------------------------------------------------
181
- Copy icon button
181
+ Copy icon button and feedback icons
182
182
  ----------------------------------------------------------------------- */
183
- .oa-copy-icon {
183
+ .oa-copy-icon,
184
+ .oa-feedback-icon {
184
185
  display: inline-flex;
185
186
  align-items: center;
186
187
  justify-content: center;
@@ -196,11 +197,69 @@
196
197
  padding: 0;
197
198
  }
198
199
 
199
- .oa-copy-icon:hover {
200
+ .oa-copy-icon:hover,
201
+ .oa-feedback-icon:hover {
200
202
  background: #f9fafb;
201
203
  color: #374151;
202
204
  }
203
205
 
206
+ .oa-feedback-icon--positive {
207
+ background: #dcfce7 !important;
208
+ border-color: #86efac !important;
209
+ color: #16a34a !important;
210
+ }
211
+
212
+ .oa-feedback-icon--negative {
213
+ background: #fee2e2 !important;
214
+ border-color: #fca5a5 !important;
215
+ color: #dc2626 !important;
216
+ }
217
+
218
+ .oa-feedback-icon:disabled {
219
+ opacity: 0.5;
220
+ cursor: default;
221
+ }
222
+
223
+ .oa-feedback-icon--positive:disabled,
224
+ .oa-feedback-icon--negative:disabled {
225
+ opacity: 1;
226
+ }
227
+
228
+ /* Feedback reason pill buttons */
229
+ .oa-feedback-reason-btn {
230
+ display: inline-flex;
231
+ align-items: center;
232
+ padding: 4px 12px;
233
+ font-size: 12px;
234
+ font-weight: 500;
235
+ line-height: 18px;
236
+ color: #991b1b;
237
+ background: #fef2f2;
238
+ border: 1px solid #fecaca;
239
+ border-radius: 16px;
240
+ cursor: pointer;
241
+ transition:
242
+ background 0.15s,
243
+ border-color 0.15s;
244
+ font-family: inherit;
245
+ }
246
+
247
+ .oa-feedback-reason-btn:hover:not(:disabled) {
248
+ background: #fee2e2;
249
+ border-color: #f87171;
250
+ }
251
+
252
+ .oa-feedback-reason-btn--selected {
253
+ background: #dc2626 !important;
254
+ color: #ffffff !important;
255
+ border-color: #dc2626 !important;
256
+ }
257
+
258
+ .oa-feedback-reason-btn:disabled {
259
+ opacity: 0.45;
260
+ cursor: default;
261
+ }
262
+
204
263
  /* -----------------------------------------------------------------------
205
264
  Toast
206
265
  ----------------------------------------------------------------------- */
@@ -13,8 +13,12 @@ export interface ExotelAIAssistParams {
13
13
  /** A single AI-generated suggestion for the agent. */
14
14
  export interface Suggestion {
15
15
  id: string;
16
+ text: string;
16
17
  value: string;
17
18
  timestamp: number;
19
+ sequence: number;
20
+ feedbackType: "good" | "bad" | null;
21
+ badFeedbackReason: string | null;
18
22
  }
19
23
 
20
24
  /** A single spoken line as received in the live transcript. */
@@ -53,7 +57,11 @@ export type StreamState = "connected" | "throttled" | "pending" | "disconnected"
53
57
  export interface BotConfig {
54
58
  sentiment: boolean;
55
59
  transcript: boolean;
56
- suggestions: boolean;
60
+ suggestion: {
61
+ enabled: boolean;
62
+ feedback_enabled: boolean;
63
+ bad_feedback_options?: string[];
64
+ };
57
65
  status: "LIVE" | "DRAFT" | "DEACTIVATED";
58
66
  }
59
67
 
@@ -69,10 +77,23 @@ interface TranscriptMessage {
69
77
  transcript_segments: TranscriptSegment[];
70
78
  }
71
79
 
80
+ export interface SuggestionValue {
81
+ text: string;
82
+ sequence: number;
83
+ }
84
+
72
85
  export interface WssEvent {
73
86
  event_type: "suggestion" | "sentiment" | "transcript";
74
87
  transcript: TranscriptMessage[];
75
- value: string;
88
+ text: string;
89
+ value?: SuggestionValue | string;
90
+ }
91
+
92
+ export interface SuggestionFeedbackMessage {
93
+ type: "suggestion_feedback";
94
+ sequence: number;
95
+ feedback_type: "good" | "bad" | null;
96
+ bad_feedback_reason: string | null;
76
97
  }
77
98
 
78
99
  export interface InitialHandshakeResponse {