@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.
- package/dist/components/key-components/SuggestionTab.d.ts +2 -1
- package/dist/controller/index.d.ts +1 -0
- package/dist/controller/index.js +37 -8
- package/dist/hooks/useExotelAIAssist.d.ts +1 -0
- package/dist/index.js +188 -48
- package/dist/react/index.js +188 -48
- package/dist/types/index.d.ts +21 -2
- package/package.json +1 -1
- package/src/components/ExotelAIAssistApp.tsx +4 -4
- package/src/components/key-components/SuggestionTab.tsx +101 -15
- package/src/controller/index.ts +42 -21
- package/src/hooks/useExotelAIAssist.ts +17 -1
- package/src/styles/index.css +62 -3
- package/src/types/index.ts +23 -2
|
@@ -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;
|
package/dist/controller/index.js
CHANGED
|
@@ -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"
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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:
|
|
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"
|
|
21211
|
-
const
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
|
|
21216
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
21745
|
-
|
|
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
|
-
|
|
21870
|
+
display: "flex",
|
|
21871
|
+
flexDirection: "column",
|
|
21872
|
+
gap: "4px",
|
|
21873
|
+
flexShrink: 0,
|
|
21874
|
+
marginTop: "10px"
|
|
21767
21875
|
},
|
|
21768
|
-
children:
|
|
21769
|
-
|
|
21770
|
-
|
|
21771
|
-
|
|
21772
|
-
|
|
21773
|
-
|
|
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
|
-
|
|
21776
|
-
|
|
21909
|
+
display: "flex",
|
|
21910
|
+
gap: "8px",
|
|
21911
|
+
flexWrap: "wrap",
|
|
21912
|
+
marginTop: "8px"
|
|
21777
21913
|
},
|
|
21778
|
-
children: jsxRuntimeExports.jsx(
|
|
21779
|
-
|
|
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",
|
package/dist/react/index.js
CHANGED
|
@@ -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"
|
|
6497
|
-
const
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
7031
|
-
|
|
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
|
-
|
|
7156
|
+
display: "flex",
|
|
7157
|
+
flexDirection: "column",
|
|
7158
|
+
gap: "4px",
|
|
7159
|
+
flexShrink: 0,
|
|
7160
|
+
marginTop: "10px"
|
|
7053
7161
|
},
|
|
7054
|
-
children:
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
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
|
-
|
|
7062
|
-
|
|
7195
|
+
display: "flex",
|
|
7196
|
+
gap: "8px",
|
|
7197
|
+
flexWrap: "wrap",
|
|
7198
|
+
marginTop: "8px"
|
|
7063
7199
|
},
|
|
7064
|
-
children: jsxRuntime.jsx(
|
|
7065
|
-
|
|
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",
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
@@ -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 {
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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?.
|
|
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}
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
<div className={
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
})}
|
package/src/controller/index.ts
CHANGED
|
@@ -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"
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|
package/src/styles/index.css
CHANGED
|
@@ -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
|
----------------------------------------------------------------------- */
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|