mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-12 19:31:13 +00:00
Compare commits
5 Commits
main
...
feat/noz-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ef6d478a | ||
|
|
4770657167 | ||
|
|
b5a8e7fb0a | ||
|
|
5a9bbc0e64 | ||
|
|
09784eec6c |
@@ -1,37 +1,5 @@
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
.panel:global(.floating-panel) {
|
||||
box-shadow: var(--shadow-dialog);
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
@@ -7,6 +7,8 @@ import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, X } from '@signozhq/icons';
|
||||
import Noz from 'components/Noz/Noz';
|
||||
import { BOTTOM_STRIP_HEIGHT } from 'container/BottomStrip';
|
||||
import { FloatingPanel } from 'periscope/components/FloatingPanel';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
@@ -22,6 +24,22 @@ import styles from './AIAssistantPanel.module.scss';
|
||||
const AI_ASSISTANT_PANEL_OPEN_CLASS = 'ai-assistant-panel-open';
|
||||
const AI_ASSISTANT_PANEL_WIDTH_VAR = '--ai-assistant-panel-width';
|
||||
|
||||
const PANEL_WIDTH = 420;
|
||||
const PANEL_HEIGHT = 601;
|
||||
const PANEL_MIN_WIDTH = 380;
|
||||
const PANEL_MIN_HEIGHT = 500;
|
||||
const PANEL_MARGIN = 8;
|
||||
const PANEL_RESIZE_EDGES = {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
export default function AIAssistantPanel(): JSX.Element | null {
|
||||
const { pathname } = useAppLocation();
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
@@ -72,11 +90,6 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
setShowHistory(false);
|
||||
}, []);
|
||||
|
||||
// ── Resize logic ──────────────────────────────────────────────────────────
|
||||
const [panelWidth, setPanelWidth] = useState(380);
|
||||
const dragStartX = useRef(0);
|
||||
const dragStartWidth = useRef(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const shouldOffsetChatSupport = isOpen && !isFullScreenPage;
|
||||
|
||||
@@ -88,7 +101,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
if (shouldOffsetChatSupport) {
|
||||
document.body.style.setProperty(
|
||||
AI_ASSISTANT_PANEL_WIDTH_VAR,
|
||||
`${panelWidth}px`,
|
||||
`${PANEL_WIDTH}px`,
|
||||
);
|
||||
} else {
|
||||
document.body.style.removeProperty(AI_ASSISTANT_PANEL_WIDTH_VAR);
|
||||
@@ -98,46 +111,40 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
document.body.classList.remove(AI_ASSISTANT_PANEL_OPEN_CLASS);
|
||||
document.body.style.removeProperty(AI_ASSISTANT_PANEL_WIDTH_VAR);
|
||||
};
|
||||
}, [isFullScreenPage, isOpen, panelWidth]);
|
||||
|
||||
const handleResizeMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
dragStartX.current = e.clientX;
|
||||
dragStartWidth.current = panelWidth;
|
||||
|
||||
const onMouseMove = (ev: MouseEvent): void => {
|
||||
// Panel is on the right; dragging left (lower clientX) increases width
|
||||
const delta = dragStartX.current - ev.clientX;
|
||||
const next = Math.min(Math.max(dragStartWidth.current + delta, 380), 800);
|
||||
setPanelWidth(next);
|
||||
};
|
||||
|
||||
const onMouseUp = (): void => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
},
|
||||
[panelWidth],
|
||||
);
|
||||
}, [isFullScreenPage, isOpen]);
|
||||
|
||||
if (!isOpen || isFullScreenPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panelHeight = Math.max(
|
||||
PANEL_MIN_HEIGHT,
|
||||
Math.min(
|
||||
PANEL_HEIGHT,
|
||||
window.innerHeight - BOTTOM_STRIP_HEIGHT - 2 * PANEL_MARGIN,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<VariantContext.Provider value="panel">
|
||||
<div className={styles.panel} style={{ width: panelWidth }}>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.resizeHandle} onMouseDown={handleResizeMouseDown} />
|
||||
<div className={styles.header}>
|
||||
<FloatingPanel
|
||||
isOpen
|
||||
className={styles.panel}
|
||||
width={PANEL_WIDTH}
|
||||
height={panelHeight}
|
||||
minWidth={PANEL_MIN_WIDTH}
|
||||
minHeight={PANEL_MIN_HEIGHT}
|
||||
enableResizing={PANEL_RESIZE_EDGES}
|
||||
defaultPosition={{
|
||||
x: window.innerWidth - PANEL_WIDTH - PANEL_MARGIN,
|
||||
// Never above the viewport, where the header could not be dragged back.
|
||||
y: Math.max(
|
||||
0,
|
||||
window.innerHeight - BOTTOM_STRIP_HEIGHT - PANEL_MARGIN - panelHeight,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className={`${styles.header} floating-panel__drag-handle`}>
|
||||
<div className={`${styles.title} noz-wave`}>
|
||||
<Noz size={18} />
|
||||
<span>Noz</span>
|
||||
@@ -207,7 +214,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
<ConversationView conversationId={activeConversationId} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</FloatingPanel>
|
||||
</VariantContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
&.compact {
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,32 @@
|
||||
.input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: var(--l1-background);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-3);
|
||||
box-shadow: var(--shadow-dialog);
|
||||
position: relative;
|
||||
|
||||
// The panel already frames it.
|
||||
&.compact {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 10px 0;
|
||||
}
|
||||
|
||||
.contextTags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 10px 0;
|
||||
}
|
||||
|
||||
.contextTag {
|
||||
@@ -91,72 +99,53 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.composer {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 4px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
// Scope the focus ring to the textarea row only — the surrounding
|
||||
// chrome (context chips, "Add Context", mic, send) sits outside this
|
||||
// element and stays unaffected when the cursor enters the textarea.
|
||||
&:focus-within {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 1px
|
||||
color-mix(in srgb, var(--accent-primary), transparent 70%);
|
||||
}
|
||||
padding: 9px 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 10px 8px;
|
||||
}
|
||||
|
||||
.leftActions,
|
||||
.rightActions {
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
--button-height: 28px;
|
||||
--button-width: 28px;
|
||||
--button-max-width: 28px;
|
||||
--button-border-radius: 9999px;
|
||||
// Send rests grey and turns primary on hover.
|
||||
--button-primary-background-color: var(--l3-background);
|
||||
--button-primary-hover-background-color: var(--primary-background);
|
||||
--button-primary-solid-foreground: var(--l2-foreground);
|
||||
|
||||
.attachBtn {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.contextBtn {
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
border-style: dashed !important;
|
||||
padding-inline: 12px !important;
|
||||
[data-color='primary']:hover:not([aria-disabled='true']) {
|
||||
--button-primary-solid-foreground: var(--primary-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
overflow-y: auto;
|
||||
font-family: inherit;
|
||||
@include scrollbar(0.2rem);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -170,7 +159,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
margin: 0 10px;
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -180,24 +169,9 @@
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-2);
|
||||
|
||||
&.stop {
|
||||
background: var(--accent-cherry) !important;
|
||||
border-color: var(--accent-cherry) !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextPopover {
|
||||
width: 480px !important;
|
||||
max-width: min(92vw, 480px);
|
||||
margin-left: 16px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--l1-background);
|
||||
@@ -342,9 +316,8 @@
|
||||
.micRecording {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--l2-background);
|
||||
background: var(--l3-background);
|
||||
border-radius: 999px;
|
||||
padding: 2px;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -46,12 +46,13 @@ import {
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
||||
import { MessageAttachment } from '../../types';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { MessageContext } from '../../../../api/ai-assistant/chat';
|
||||
import {
|
||||
ArrowUp,
|
||||
AtSign,
|
||||
Mic,
|
||||
Plus,
|
||||
Search,
|
||||
Send,
|
||||
Square,
|
||||
TriangleAlert,
|
||||
X,
|
||||
@@ -249,6 +250,7 @@ export default function ChatInput({
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const isCompact = useVariant() === 'panel';
|
||||
const [text, setText] = useState('');
|
||||
const [pendingFiles, setPendingFiles] = useState<UploadFile[]>([]);
|
||||
const [selectedContexts, setSelectedContexts] = useState<
|
||||
@@ -900,7 +902,10 @@ export default function ChatInput({
|
||||
const showTextWarning = currentLength >= WARNING_THRESHOLD;
|
||||
|
||||
return (
|
||||
<div className={styles.input} ref={inputRootRef}>
|
||||
<div
|
||||
className={cx(styles.input, { [styles.compact]: isCompact })}
|
||||
ref={inputRootRef}
|
||||
>
|
||||
{pendingFiles.length > 0 && (
|
||||
<div className={styles.attachments}>
|
||||
{pendingFiles.map((f) => (
|
||||
@@ -983,7 +988,7 @@ export default function ChatInput({
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={styles.textarea}
|
||||
placeholder="Ask anything… (Shift+Enter for new line)"
|
||||
placeholder="Ask Noz…"
|
||||
value={text}
|
||||
onChange={(e): void => {
|
||||
const next = capText(e.target.value);
|
||||
@@ -995,7 +1000,7 @@ export default function ChatInput({
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
maxLength={MAX_INPUT_LENGTH}
|
||||
rows={3}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
{showTextWarning && (
|
||||
@@ -1009,7 +1014,69 @@ export default function ChatInput({
|
||||
)}
|
||||
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.leftActions}>
|
||||
<div className={styles.actions}>
|
||||
{showMic &&
|
||||
(isListening ? (
|
||||
<div
|
||||
className={styles.micRecording}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Recording voice input"
|
||||
>
|
||||
<Tooltip title="Discard recording">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<span className={styles.micWaves} aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<Tooltip title="Stop and send">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
>
|
||||
<Square size={9} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Voice input">
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon
|
||||
onClick={(): void => startVoiceInput(VoiceInputSource.Button)}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
<Popover
|
||||
open={isContextPickerOpen}
|
||||
onOpenChange={(open): void => {
|
||||
@@ -1020,22 +1087,25 @@ export default function ChatInput({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="md"
|
||||
disabled={disabled}
|
||||
onClick={(): void => {
|
||||
setActiveContextCategory('Dashboards');
|
||||
setPickerSearchQuery('');
|
||||
}}
|
||||
prefix={<Plus size={10} />}
|
||||
>
|
||||
Add Context
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Tooltip title="Add context">
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
disabled={disabled}
|
||||
onClick={(): void => {
|
||||
setActiveContextCategory('Dashboards');
|
||||
setPickerSearchQuery('');
|
||||
}}
|
||||
aria-label="Add context"
|
||||
>
|
||||
<AtSign size={14} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</Tooltip>
|
||||
<PopoverContent
|
||||
className={styles.contextPopover}
|
||||
side="top"
|
||||
@@ -1164,82 +1234,18 @@ export default function ChatInput({
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightActions}>
|
||||
{showMic &&
|
||||
(isListening ? (
|
||||
<div
|
||||
className={styles.micRecording}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Recording voice input"
|
||||
>
|
||||
<Tooltip title="Discard recording">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<span className={styles.micWaves} aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<Tooltip title="Stop and send">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
>
|
||||
<Square size={9} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Voice input">
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
icon
|
||||
onClick={(): void => startVoiceInput(VoiceInputSource.Button)}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{isStreaming && onCancel ? (
|
||||
<Tooltip title="Stop generating">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
size="sm"
|
||||
icon
|
||||
color="danger"
|
||||
onClick={onCancel}
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<Square size={10} fill="currentColor" strokeWidth={0} />
|
||||
<Square size={14} fill="currentColor" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -1247,14 +1253,14 @@ export default function ChatInput({
|
||||
<Button
|
||||
disabledTooltip={disabled ? disabledTooltip : 'Type a message first'}
|
||||
variant="solid"
|
||||
size="md"
|
||||
size="sm"
|
||||
icon
|
||||
color="primary"
|
||||
onClick={isListening ? handleStopAndSend : handleSend}
|
||||
disabled={disabled || (!text.trim() && pendingFiles.length === 0)}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Send size={14} />
|
||||
<ArrowUp size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import ChatInput from '../ChatInput';
|
||||
|
||||
function getComposer(): HTMLTextAreaElement {
|
||||
return screen.getByPlaceholderText(/Ask anything/i) as HTMLTextAreaElement;
|
||||
return screen.getByPlaceholderText(/Ask Noz/i) as HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
describe('ChatInput — @ mention trigger', () => {
|
||||
|
||||
@@ -58,7 +58,7 @@ function renderChatInput(): void {
|
||||
}
|
||||
|
||||
function getComposer(): HTMLTextAreaElement {
|
||||
return screen.getByPlaceholderText(/Ask anything/i) as HTMLTextAreaElement;
|
||||
return screen.getByPlaceholderText(/Ask Noz/i) as HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
describe('ChatInput — empty-state CTA prefill flow', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { screen, userEvent } from 'storybook/test';
|
||||
import { screen, userEvent, within } from 'storybook/test';
|
||||
|
||||
import type { GlobalMockArgs } from '../globals';
|
||||
|
||||
@@ -72,6 +72,19 @@ export const NozAwaitingYou: Story = {
|
||||
args: { noz: true, nozAwaiting: 2, support: 'pylon' },
|
||||
};
|
||||
|
||||
/** The Noz chat opened from Ask Noz, floating above the strip; its header drags it. */
|
||||
export const NozPanel: Story = {
|
||||
args: { noz: true, support: 'pylon' },
|
||||
play: async (): Promise<void> => {
|
||||
await userEvent.click(
|
||||
within(
|
||||
await screen.findByTestId('bottom-strip-ask-noz', undefined, untilLoaded),
|
||||
).getByRole('button'),
|
||||
);
|
||||
await screen.findByRole('button', { name: 'Close panel' });
|
||||
},
|
||||
};
|
||||
|
||||
/** Every tooltip the strip carries, held open. */
|
||||
export const Tooltips: Story = {
|
||||
args: { noz: true, support: 'pylon', tooltipsOpen: true },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AI_API_PATH } from 'api/AIAPIInstance';
|
||||
import type { ThreadListResponse } from 'api/ai-assistant/chat';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import type { ConversationStreamState } from 'container/AIAssistant/types';
|
||||
import { chipsResponse } from 'pages/AIAssistantPage/stories/__story_mockdata__/aiAssistant';
|
||||
|
||||
import { countControl, toggleControl } from '../controls/controls';
|
||||
import { defineStoryMocks } from '../controls/defineStoryMocks';
|
||||
@@ -57,6 +58,10 @@ export const nozMocks = defineStoryMocks({
|
||||
`${nozGlobalConfigResponse.data.ai_assistant_url}${AI_API_PATH}/threads`,
|
||||
(_req, res, ctx) => res(ctx.status(200), ctx.json(noThreads)),
|
||||
),
|
||||
rest.get(
|
||||
`${nozGlobalConfigResponse.data.ai_assistant_url}${AI_API_PATH}/empty-state/chips`,
|
||||
(_req, res, ctx) => res(ctx.status(200), ctx.json(chipsResponse())),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
effect: ({ nozAwaiting }) => {
|
||||
@@ -72,6 +77,9 @@ export const nozMocks = defineStoryMocks({
|
||||
for (let index = 0; index < nozAwaiting; index += 1) {
|
||||
state.streams[`${AWAITING_PREFIX}${index}`] = awaitingStream();
|
||||
}
|
||||
|
||||
// Persisted, so a panel one story opened would hide Ask Noz in the next.
|
||||
state.isDrawerOpen = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user