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
2 changes: 2 additions & 0 deletions changelogs/fragments/11330.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Update chat-header look & feel ([#11330](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/11330))
30 changes: 28 additions & 2 deletions src/plugins/chat/public/components/chat_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,38 @@
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid $euiColorLightShade;
gap: 16px;
overflow-x: hidden;

&__titleGroup {
gap: 8px;
flex: 1;
min-width: 0;
overflow: hidden;
}

&__titleContainer {
min-width: 0;
overflow: hidden;
}

&__chatIcon {
color: $euiColorDarkShade;
}

&__title {
margin: 0;
font-weight: 600;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

&__buttons {
display: flex;
gap: 8px;
gap: 4px;
align-items: center;
flex-shrink: 0;
}

&__experimentalBadge {
Expand Down
56 changes: 26 additions & 30 deletions src/plugins/chat/public/components/chat_header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,57 @@
*/

import React from 'react';
import { EuiText, EuiButtonIcon, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ChatLayoutMode } from './chat_header_button';
import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import './chat_header.scss';

interface ChatHeaderProps {
layoutMode: ChatLayoutMode;
conversationName?: string;
isStreaming: boolean;
onToggleLayout?: () => void;
onNewChat: () => void;
onClose: () => void;
}

export const ChatHeader: React.FC<ChatHeaderProps> = ({
layoutMode,
conversationName = '',
isStreaming,
onToggleLayout,
onNewChat,
onClose,
}) => {
return (
<div className="chatHeader">
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexGroup
alignItems="center"
gutterSize="none"
responsive={false}
className="chatHeader__titleGroup"
>
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>Ask AI</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color="warning" className="chatHeader__experimentalBadge">
Experimental
</EuiBadge>
<EuiIcon type="chatLeft" size="m" className="chatHeader__chatIcon" />
</EuiFlexItem>
{conversationName && (
<EuiFlexItem grow={true} className="chatHeader__titleContainer">
<EuiText size="m">
<h3 className="chatHeader__title">{conversationName}</h3>
</EuiText>
</EuiFlexItem>
)}
Comment on lines 31 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the EUI version in package.json
find . -name "package.json" -path "*/package.json" | head -5 | xargs grep -l "@elastic/eui" | head -1 | xargs cat | grep -A2 "@elastic/eui"

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 359


🏁 Script executed:

# Search for chatLeft and documentEdit icon references
rg -n "chatLeft|documentEdit" --type=ts --type=tsx -g '!node_modules' | head -20

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 111


🏁 Script executed:

# Check if there are any custom icon definitions or registrations
rg -n "icon.*register|customIcon|registerIcon" --type=ts --type=tsx -g '!node_modules' | head -20

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 111


🏁 Script executed:

# Look at the chat_header.tsx file to see the full context
cat -n src/plugins/chat/public/components/chat_header.tsx

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2075


🏁 Script executed:

# Search for icon type definitions or icon lists in the codebase
rg "chatLeft|documentEdit" -i --max-count=20

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 299


🏁 Script executed:

# Check node_modules for OUI icon types (if available)
find . -path "*/node_modules/@opensearch-project/oui*" -name "*.d.ts" 2>/dev/null | grep -i icon | head -5

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 66


🏁 Script executed:

# Search for other icon usages in the codebase to see what's common
rg "type=\"[a-zA-Z]+" -o | sort | uniq -c | sort -rn | head -20

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2856


🏁 Script executed:

# Look for the EuiIcon component definition or imports to understand its types
rg "EuiIcon" -A5 -B5 | head -40

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 4336


🏁 Script executed:

# Search for icon type definitions in the codebase
rg "iconType|type.*icon" -i | grep -E "(documentEdit|chatLeft)" | head -10

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 299


🏁 Script executed:

# Check if there are TypeScript errors or type definition files
find . -name "*.d.ts" -path "*/eui/*" -o -path "*/oui/*" | head -10

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 197


🏁 Script executed:

# Look for what icon types are typically used with EuiIcon and EuiButtonIcon
rg "iconType=\"[^\"]+\"|type=\"[^\"]+\"" -o --max-count=30 | sort | uniq -c | sort -rn | head -20

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2989


🏁 Script executed:

# Check if there's a types file or definition for icon types
rg "chatLeft|documentEdit" --type ts -i

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 299


🏁 Script executed:

# Check the EUI type definitions for icon types
cat -n ./typings/@elastic/eui/index.d.ts | head -100

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 1544


🏁 Script executed:

# Search for EuiIcon type definitions specifically
rg "EuiIcon|iconType" ./typings/@elastic/eui/index.d.ts -A5 -B5 | head -50

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 66


🏁 Script executed:

# Look for icon type definitions in the typings
grep -n "chatLeft\|documentEdit\|IconType" ./typings/@elastic/eui/index.d.ts

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 66


🏁 Script executed:

# Check if there are test files for the chat_header component
find . -name "*chat_header*" -type f | grep -v node_modules

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 376


🏁 Script executed:

# Look for any OUI/EUI icon constants or enumerations
rg "export.*icon|icon.*=" --type ts | grep -i "type\|const" | head -20

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2507


🏁 Script executed:

# Search for known icon types used in the codebase to understand the pattern
rg "iconType=\"[^\"]+\"" -o | sort | uniq | head -30

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2742


🏁 Script executed:

# Check web for OUI 1.22.1 icon availability
# First, let's look for node_modules artifacts or documentation
ls -la node_modules/@opensearch-project/oui/ 2>/dev/null | head -20 || echo "node_modules not available"

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 66


🏁 Script executed:

# Check the test file for chat_header to see if there are any icon-related tests
cat -n ./src/plugins/chat/public/components/chat_header_button.test.tsx | head -50

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2028


🏁 Script executed:

# Look at package.json to verify the exact OUI version and check if there's a changelog
grep -A2 "@opensearch-project/oui\|@elastic/eui" package.json

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 359


🏁 Script executed:

# Search for common icon types in OUI to build a reference list
rg "iconType=\"(copy|edit|chat|document)" -o | sort | uniq

Repository: opensearch-project/OpenSearch-Dashboards

Length of output: 2297


Use valid OUI icon types for EuiIcon and EuiButtonIcon.

The icon types chatLeft (line 32) and documentEdit (line 44) do not appear to be standard icons in OUI 1.22.1. The codebase uses separate icon types like document and edit individually, never combined. Consider using valid OUI icon types such as document or documentEdit if it's a legacy alias, or register custom icons if needed.

🤖 Prompt for AI Agents
In `@src/plugins/chat/public/components/chat_header.tsx` around lines 31 - 40,
Replace nonstandard OUI icon names used in the chat header components: update
the EuiIcon instance using type="chatLeft" (in the ChatHeader render) to a valid
OUI icon name (e.g., "chat" or another supported chat-related icon) and update
the EuiButtonIcon usage that uses "documentEdit" to either "document" or "edit"
(or split into separate icons) or register a custom icon if you require a
combined glyph; ensure you change the type prop on the EuiIcon and EuiButtonIcon
elements (referenced as EuiIcon and EuiButtonIcon in this file) to valid OUI
icon names or to a custom-registered icon name so the icons render correctly.

</EuiFlexGroup>
<div className="chatHeader__buttons">
{onToggleLayout && (
<EuiButtonIcon
iconType={layoutMode === ChatLayoutMode.FULLSCREEN ? 'minimize' : 'fullScreen'}
onClick={onToggleLayout}
disabled={isStreaming}
aria-label={
layoutMode === ChatLayoutMode.FULLSCREEN
? 'Switch to sidecar'
: 'Switch to fullscreen'
}
size="m"
/>
)}
<EuiButtonIcon
iconType="plus"
iconType="documentEdit"
onClick={onNewChat}
disabled={isStreaming}
aria-label="New chat"
size="m"
color="text"
/>
<EuiButtonIcon
iconType="cross"
onClick={onClose}
aria-label="Close chatbot"
size="m"
color="text"
/>
<EuiButtonIcon iconType="cross" onClick={onClose} aria-label="Close chatbot" size="m" />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ describe('ChatHeaderButton', () => {
);

expect(mockChatService.isWindowOpen).toHaveBeenCalled();
expect(mockChatService.getWindowMode).toHaveBeenCalled();
});

it('should initialize with window open state from ChatService', () => {
Expand Down
48 changes: 8 additions & 40 deletions src/plugins/chat/public/components/chat_header_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export const ChatHeaderButton = React.forwardRef<ChatHeaderButtonInstance, ChatH
) => {
// Use ChatService as source of truth for window state
const [isOpen, setIsOpen] = useState<boolean>(chatService.isWindowOpen());
const [layoutMode, setLayoutMode] = useState<ChatLayoutMode>(chatService.getWindowMode());
const sideCarRef = useRef<{ close: () => void }>();
const chatWindowRef = useRef<ChatWindowInstance>(null);
const flyoutMountPoint = useRef(null);
Expand All @@ -71,12 +70,9 @@ export const ChatHeaderButton = React.forwardRef<ChatHeaderButtonInstance, ChatH

try {
sideCarRef.current = core.overlays.sidecar.open(flyoutMountPoint.current, {
className: `chat-sidecar chat-sidecar--${layoutMode}`,
className: 'chat-sidecar chat-sidecar--sidecar',
config: {
dockedMode:
layoutMode === ChatLayoutMode.FULLSCREEN
? SIDECAR_DOCKED_MODE.TAKEOVER
: SIDECAR_DOCKED_MODE.RIGHT,
dockedMode: SIDECAR_DOCKED_MODE.RIGHT,
paddingSize: chatService.getPaddingSize(),
isHidden: false,
},
Expand All @@ -87,7 +83,7 @@ export const ChatHeaderButton = React.forwardRef<ChatHeaderButtonInstance, ChatH

// Notify ChatService that window is now open
chatService.setWindowState({ isWindowOpen: true });
}, [core.overlays, layoutMode, chatService]);
}, [core.overlays, chatService]);

const closeSidecar = useCallback(() => {
if (sideCarRef.current) {
Expand All @@ -106,28 +102,6 @@ export const ChatHeaderButton = React.forwardRef<ChatHeaderButtonInstance, ChatH
}
}, [isOpen, openSidecar, closeSidecar]);

const toggleLayoutMode = useCallback(() => {
const newLayoutMode =
layoutMode === ChatLayoutMode.SIDECAR ? ChatLayoutMode.FULLSCREEN : ChatLayoutMode.SIDECAR;

setLayoutMode(newLayoutMode);

// Update sidecar config dynamically if currently open
if (isOpen && sideCarRef.current) {
core.overlays.sidecar.setSidecarConfig({
dockedMode:
newLayoutMode === ChatLayoutMode.FULLSCREEN
? SIDECAR_DOCKED_MODE.TAKEOVER
: SIDECAR_DOCKED_MODE.RIGHT,
paddingSize: newLayoutMode === ChatLayoutMode.FULLSCREEN ? window.innerHeight - 50 : 400,
isHidden: false,
});
}

// Update ChatService with new layout mode
chatService.setWindowState({ windowMode: newLayoutMode });
}, [layoutMode, isOpen, chatService, core.overlays.sidecar]);

const startNewConversation = useCallback<ChatHeaderButtonInstance['startNewConversation']>(
async ({ content }) => {
openSidecar();
Expand All @@ -141,16 +115,11 @@ export const ChatHeaderButton = React.forwardRef<ChatHeaderButtonInstance, ChatH

// Listen to ChatService window state changes and sync local state
useEffect(() => {
const unsubscribe = chatService.onWindowStateChange(
({ isWindowOpen, windowMode }, changed) => {
if (changed.isWindowOpen) {
setIsOpen(isWindowOpen);
}
if (changed.windowMode) {
setLayoutMode(windowMode as ChatLayoutMode);
}
const unsubscribe = chatService.onWindowStateChange(({ isWindowOpen }, changed) => {
if (changed.isWindowOpen) {
setIsOpen(isWindowOpen);
}
);
});
return () => {
unsubscribe();
};
Expand Down Expand Up @@ -244,8 +213,7 @@ export const ChatHeaderButton = React.forwardRef<ChatHeaderButtonInstance, ChatH
confirmationService={confirmationService}
>
<ChatWindow
layoutMode={layoutMode}
onToggleLayout={toggleLayoutMode}
layoutMode={ChatLayoutMode.SIDECAR}
ref={chatWindowRef}
onClose={closeSidecar}
/>
Expand Down
28 changes: 24 additions & 4 deletions src/plugins/chat/public/components/chat_window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export interface ChatWindowInstance {

interface ChatWindowProps {
layoutMode?: ChatLayoutMode;
onToggleLayout?: () => void;
onClose: ()=>void;
}

Expand All @@ -47,7 +46,6 @@ export const ChatWindow = React.forwardRef<ChatWindowInstance, ChatWindowProps>(

const ChatWindowContent = React.forwardRef<ChatWindowInstance, ChatWindowProps>(({
layoutMode = ChatLayoutMode.SIDECAR,
onToggleLayout,
onClose,
}, ref) => {

Expand Down Expand Up @@ -356,6 +354,29 @@ const ChatWindowContent = React.forwardRef<ChatWindowInstance, ChatWindowProps>(
getActionRenderer: service.getActionRenderer,
};

// Get conversation name from first user message with text content
const conversationName = useMemo(() => {
// Find first user message that has text content
for (const msg of timeline) {
if (msg.role !== 'user') continue;

// Handle string content
if (typeof msg.content === 'string' && msg.content.trim()) {
return msg.content;
}

// Handle array content - look for text content
if (Array.isArray(msg.content)) {
const textContent = msg.content.find((item) => item.type === 'text');
if (textContent?.text && textContent.text.trim()) {
return textContent.text;
}
}
}

return '';
}, [timeline]);

useImperativeHandle(ref, ()=>({
startNewChat: ()=>handleNewChat(),
sendMessage: async ({content, messages})=>(await handleSendRef.current?.({input:content, messages}))
Expand All @@ -364,9 +385,8 @@ const ChatWindowContent = React.forwardRef<ChatWindowInstance, ChatWindowProps>(
return (
<ChatContainer layoutMode={layoutMode}>
<ChatHeader
layoutMode={layoutMode}
conversationName={conversationName}
isStreaming={isStreaming}
onToggleLayout={onToggleLayout}
onNewChat={handleNewChat}
onClose={onClose}
/>
Expand Down
Loading
Loading