Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/components/DevDocTemplate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ const DevDocTemplate: FC<DevDocTemplateProps> = (props) => {
// breadcrumsData is derived after processedNavMap is built (see useMemo below)
const [activeCategory, setActiveCategory] = useState<DocCategory>('guides');
const [showSearch, setShowSearch] = useState(false);

useEffect(() => {
window.dispatchEvent(new CustomEvent('spotter-code-suspend', { detail: { suspended: showSearch } }));
}, [showSearch]);
const [leftNavOpen, setLeftNavOpen] = useState(false);
const [keyword, updateKeyword] = useState('');
const [isPublicSiteOpen, setIsPublicSiteOpen] = useState(() => {
Expand Down Expand Up @@ -453,18 +457,16 @@ const isVersionedIframe = VERSION_DROPDOWN.some(
const customStyles = {
overlay: {
background: 'rgba(50,57,70, 0.9)',
zIndex: 10,
zIndex: 1100,
},
content: {
top: '50px',
left: 'auro',
left: 'auto',
right: 'auto',
bottom: 'auto',
width: isMaxMobileResolution ? '40%' : '100%',
margin: 'auto',
transform: `translate(${
isMaxMobileResolution ? '80%' : '0'
}, 70px)`,
transform: 'translate(0, 70px)',
border: 'none',
height: isMaxMobileResolution ? '400px' : '300px',
boxShadow: 'none',
Expand Down
1 change: 1 addition & 0 deletions src/components/Document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const Document = (props: {
const handleMouseUp = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.closest('.selection-cta-button')) return;
if (target.closest('.floating-assistant__panel, .floating-assistant__chip-ring')) return;

// If mouse didn't move (plain click, not a drag-select), don't re-show
const moved = Math.abs(e.clientX - mouseDownX) > 3 || Math.abs(e.clientY - mouseDownY) > 3;
Expand Down
16 changes: 16 additions & 0 deletions src/components/FloatingAssistant/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,19 @@ export async function* streamAgentResponse(

yield* parseSseStream(response);
}

export async function sendFeedback(
traceId: string,
observationId: string | undefined,
value: 'up' | 'down',
): Promise<void> {
const response = await fetch(`${CLOUDFLARE_URL}${API_PATHS.FEEDBACK}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ traceId, observationId, value }),
});

if (!response.ok) {
throw new Error(`Feedback API error: ${response.status}`);
}
}
1 change: 1 addition & 0 deletions src/components/FloatingAssistant/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const LOADING_PHASE_DELAYS = [0, 1200, 2800, 4800, 7000];
export const API_PATHS = {
SUGGESTED_QUESTIONS: '/suggested-questions',
AGENT: '/agent/embed-assistant',
FEEDBACK: '/agent/embed-assistant/feedback',
} as const;

export const ERROR_MESSAGES = {
Expand Down
18 changes: 17 additions & 1 deletion src/components/FloatingAssistant/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
}
}

.floating-assistant__chip-ring--suspended {
pointer-events: none;
opacity: 0.5;
}

.floating-assistant__panel {
position: fixed;
top: 108px;
Expand All @@ -90,7 +95,6 @@
rgba(100, 160, 255, 0.3) 90%
),
#fff;
border-left: 1px solid #e3e6eb;
display: flex;
flex-direction: column;
overflow: hidden;
Expand All @@ -109,6 +113,18 @@
&--embedded {
top: 0; // no header or secondary nav in embedded mode — panel takes full height
}

&--suspended {
pointer-events: none;

&::after {
content: '';
position: absolute;
inset: 0;
background: rgba(50, 57, 70, 0.4);
z-index: 1;
}
}
}

// ── Header ────────────────────────────────────────────────────────────────────
Expand Down
57 changes: 45 additions & 12 deletions src/components/FloatingAssistant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SpotterCodeLogo from './SpotterCodeLogo';
import { LOADING_PHASES, PANEL_MIN_WIDTH, PANEL_MAX_WIDTH, PANEL_DEFAULT_WIDTH, LOADING_PHASE_DELAYS, ERROR_MESSAGES } from './constants';
import { Message } from './types';
import { renderMarkdown, formatTimestamp, formatDuration, getPageId, stripMarkdown } from './helpers';
import { fetchSuggestedQuestions, streamAgentResponse } from './api';
import { fetchSuggestedQuestions, streamAgentResponse, sendFeedback } from './api';

const SparkleIcon = () => (
<Icon id={IconID.AI_SPARKLE_SELECTED} size={IconSize.XLARGE} color={IconColor.BLUE} />
Expand Down Expand Up @@ -72,7 +72,7 @@ const FloatingAssistant: React.FC = () => {
}, 250);
};

const giveFeedback = (idx: number, type: 'up' | 'down') => {
const giveFeedback = (idx: number, type: 'up' | 'down', message: Message) => {
const isUnfill = feedbackGiven[idx] === type;
setFeedbackGiven((prev: Record<number, 'up' | 'down'>) => {
const next = { ...prev };
Expand All @@ -86,6 +86,12 @@ const FloatingAssistant: React.FC = () => {
setToastExiting(false);
setShowFeedbackToast(true);
feedbackToastTimer.current = setTimeout(hideToast, 2500);

if (message.traceId) {
sendFeedback(message.traceId, message.observationId, type).catch(() => {
// Feedback is best-effort — a failed submission shouldn't disrupt the chat UI.
});
}
}
};
const [editingIndex, setEditingIndex] = useState<number | null>(null);
Expand Down Expand Up @@ -181,6 +187,22 @@ const FloatingAssistant: React.FC = () => {
const abortRef = useRef<AbortController | null>(null);
const userScrolledRef = useRef(false);

const [isSuspended, setIsSuspended] = useState(false);
const panelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handler = (e: CustomEvent<{ suspended: boolean }>) => {
setIsSuspended(e.detail.suspended);
if (e.detail.suspended) {
const active = document.activeElement as HTMLElement | null;
if (active && panelRef.current?.contains(active)) {
active.blur();
}
}
};
window.addEventListener('spotter-code-suspend', handler as EventListener);
return () => window.removeEventListener('spotter-code-suspend', handler as EventListener);
}, []);

useEffect(() => {
setIsOpen(true);
}, []);
Expand Down Expand Up @@ -217,10 +239,10 @@ const FloatingAssistant: React.FC = () => {
};

useEffect(() => {
if (isOpen && inputRef.current) {
if (isOpen && !isSuspended && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
}, [isOpen, isSuspended]);

useEffect(() => {
const el = inputRef.current;
Expand All @@ -230,10 +252,10 @@ const FloatingAssistant: React.FC = () => {
}, [input]);

useEffect(() => {
if (quotedText) {
if (quotedText && !isSuspended) {
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [quotedText]);
}, [quotedText, isSuspended]);

useEffect(() => {
const handler = (e: CustomEvent<{ location: Location }>) => {
Expand Down Expand Up @@ -318,10 +340,15 @@ const FloatingAssistant: React.FC = () => {
let collectedSteps: string[] = [];
let finalContent: string = ERROR_MESSAGES.DEFAULT;
let aborted = false;
let traceId: string | undefined;
let observationId: string | undefined;

try {
for await (const event of streamAgentResponse(updatedMessages, pageId, abortRef.current.signal)) {
if (event.type === 'text') {
if (event.type === 'trace') {
traceId = event.traceId;
observationId = event.observationId ?? event.generationId;
} else if (event.type === 'text') {
accumulated += event.content;
setStreamingText(accumulated);
} else if (event.type === 'tool-start') {
Expand All @@ -347,6 +374,8 @@ const FloatingAssistant: React.FC = () => {
content: finalContent,
toolSteps: collectedSteps.length > 0 ? collectedSteps : undefined,
durationMs: Date.now() - startTime,
traceId,
observationId,
}]);
}
setStreamingText('');
Expand Down Expand Up @@ -403,11 +432,14 @@ const FloatingAssistant: React.FC = () => {
return (
<>
{!isOpen && !isClosing && (
<div className="floating-assistant__chip-ring">
<div
className={`floating-assistant__chip-ring${isSuspended ? ' floating-assistant__chip-ring--suspended' : ''}`}
>
<button
className="floating-assistant__chip"
onClick={() => setIsOpen(true)}
aria-label="Open SpotterCode assistant"
disabled={isSuspended}
>
<SparkleIcon />
</button>
Expand All @@ -416,7 +448,8 @@ const FloatingAssistant: React.FC = () => {

{(isOpen || isClosing) && (
<div
className={`floating-assistant__panel${isClosing ? ' closing' : ''}${!isLandingPage ? ' floating-assistant__panel--conversation' : ''}${isEmbedded ? ' floating-assistant__panel--embedded' : ''}`}
ref={panelRef}
className={`floating-assistant__panel${isClosing ? ' closing' : ''}${!isLandingPage ? ' floating-assistant__panel--conversation' : ''}${isEmbedded ? ' floating-assistant__panel--embedded' : ''}${isSuspended ? ' floating-assistant__panel--suspended' : ''}`}
style={{ width: panelWidth }}
>
<div className="floating-assistant__resize-handle" onMouseDown={onResizeMouseDown} />
Expand Down Expand Up @@ -571,14 +604,14 @@ const FloatingAssistant: React.FC = () => {
<button
className={`fa-feedback-btn${feedbackGiven[i] === 'down' ? ' fa-feedback-btn--active' : ''}`}
aria-label="Thumbs down"
onClick={() => giveFeedback(i, 'down')}
onClick={() => giveFeedback(i, 'down', msg)}
>
<Icon id={feedbackGiven[i] === 'down' ? IconID.THUMB_DOWN_UNDO : IconID.THUMB_DOWN} size={IconSize.SMALL} color={feedbackGiven[i] === 'down' ? IconColor.BLUE : IconColor.GRAY} />
</button>
<button
className={`fa-feedback-btn${feedbackGiven[i] === 'up' ? ' fa-feedback-btn--active' : ''}`}
aria-label="Thumbs up"
onClick={() => giveFeedback(i, 'up')}
onClick={() => giveFeedback(i, 'up', msg)}
>
<Icon id={feedbackGiven[i] === 'up' ? IconID.THUMB_UP_UNDO : IconID.THUMB_UP} size={IconSize.SMALL} color={feedbackGiven[i] === 'up' ? IconColor.BLUE : IconColor.GRAY} />
</button>
Expand Down Expand Up @@ -679,7 +712,7 @@ const FloatingAssistant: React.FC = () => {
</div>

<div className="floating-assistant__footer">
SpotterCode responses should be reviewed.{' '}
You are interacting with an AI system. Responses should be reviewed.{' '}
<a href="https://developers.thoughtspot.com/docs/SpotterCode" target="_blank" rel="noopener noreferrer">
Learn more
</a>
Expand Down
3 changes: 3 additions & 0 deletions src/components/FloatingAssistant/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ export type Message = {
toolSteps?: string[];
durationMs?: number;
sentAt?: number;
traceId?: string;
observationId?: string;
};

export type SseEvent =
| { type: 'trace'; traceId: string; generationId?: string; observationId?: string }
| { type: 'text'; content: string }
| { type: 'tool-start'; toolName: string; content: string; input: unknown }
| { type: 'tool-result'; toolName: string; content: string; output: unknown }
Expand Down
Loading