feat(conversation): Add resizable embedding projector panel with improved UX

- Larger default size (700x550) for better readability
- Resizable from all 8 edges/corners with visual SE grip indicator
- Clearer button icons (18px, strokeWidth 2.5)
- Draggable, minimizable, pinnable panel
- Dark theme and mobile responsive support
This commit is contained in:
kempersc 2025-12-15 17:45:27 +01:00
parent d9892dba6f
commit 82aa655522
3 changed files with 838 additions and 0 deletions

View file

@ -0,0 +1,360 @@
/**
* ConversationEmbeddingPanel.css
*
* Styles for the draggable, resizable embedding projector panel.
* Based on InstitutionInfoPanel.css and RdfNodeDetailsPanel.css patterns.
*/
/* ============================================================================
Main Panel Container
============================================================================ */
.conversation-embedding-panel {
position: fixed;
top: 0;
left: 0;
background: white;
border: 1px solid var(--border-color, #e5e5e5);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 150;
transition: box-shadow 0.2s ease;
will-change: transform;
}
.conversation-embedding-panel--dragging,
.conversation-embedding-panel--resizing {
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.18);
cursor: grabbing !important;
}
.conversation-embedding-panel--minimized {
height: 48px !important;
}
.conversation-embedding-panel--simple {
/* Width/height handled inline by component */
}
/* ============================================================================
Resize Handles
============================================================================ */
.conversation-embedding-panel__resize-handle {
position: absolute;
background: transparent;
z-index: 10;
}
/* North and South edges */
.conversation-embedding-panel__resize-handle--n,
.conversation-embedding-panel__resize-handle--s {
left: 12px;
right: 12px;
height: 8px;
cursor: ns-resize;
}
.conversation-embedding-panel__resize-handle--n {
top: -4px;
}
.conversation-embedding-panel__resize-handle--s {
bottom: -4px;
}
/* East and West edges */
.conversation-embedding-panel__resize-handle--e,
.conversation-embedding-panel__resize-handle--w {
top: 12px;
bottom: 12px;
width: 8px;
cursor: ew-resize;
}
.conversation-embedding-panel__resize-handle--e {
right: -4px;
}
.conversation-embedding-panel__resize-handle--w {
left: -4px;
}
/* Corner handles */
.conversation-embedding-panel__resize-handle--ne,
.conversation-embedding-panel__resize-handle--nw,
.conversation-embedding-panel__resize-handle--se,
.conversation-embedding-panel__resize-handle--sw {
width: 16px;
height: 16px;
}
.conversation-embedding-panel__resize-handle--ne {
top: -4px;
right: -4px;
cursor: nesw-resize;
}
.conversation-embedding-panel__resize-handle--nw {
top: -4px;
left: -4px;
cursor: nwse-resize;
}
.conversation-embedding-panel__resize-handle--se {
bottom: -4px;
right: -4px;
cursor: nwse-resize;
}
.conversation-embedding-panel__resize-handle--sw {
bottom: -4px;
left: -4px;
cursor: nesw-resize;
}
/* Visual indicator for SE corner (most commonly used for resize) */
.conversation-embedding-panel__resize-handle--se::after {
content: '';
position: absolute;
right: 6px;
bottom: 6px;
width: 12px;
height: 12px;
border-right: 2px solid rgba(0, 0, 0, 0.15);
border-bottom: 2px solid rgba(0, 0, 0, 0.15);
border-radius: 0 0 3px 0;
pointer-events: none;
}
.conversation-embedding-panel__resize-handle--se:hover::after {
border-color: rgba(10, 61, 250, 0.4);
}
/* ============================================================================
Header (Drag Handle)
============================================================================ */
.conversation-embedding-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: none;
background: linear-gradient(135deg, #0a3dfa 0%, #1e50ff 100%);
flex-shrink: 0;
user-select: none;
}
.conversation-embedding-panel--minimized .conversation-embedding-panel__header {
border-bottom: none;
}
.conversation-embedding-panel__title {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9375rem;
font-weight: 600;
color: white;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-embedding-panel__title svg {
color: rgba(255, 255, 255, 0.9);
flex-shrink: 0;
}
/* ============================================================================
Controls - Larger, clearer buttons
============================================================================ */
.conversation-embedding-panel__controls {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.conversation-embedding-panel__sources-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
margin-right: 4px;
}
.conversation-embedding-panel__control-btn,
.conversation-embedding-panel__close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
color: white;
cursor: pointer;
transition: all 0.15s ease;
}
.conversation-embedding-panel__control-btn svg,
.conversation-embedding-panel__close-btn svg {
width: 18px;
height: 18px;
}
.conversation-embedding-panel__control-btn:hover {
background: rgba(255, 255, 255, 0.35);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.05);
}
.conversation-embedding-panel__control-btn--active {
background: rgba(255, 255, 255, 0.4);
border-color: rgba(255, 255, 255, 0.6);
}
.conversation-embedding-panel__close-btn:hover {
background: rgba(220, 38, 38, 0.9);
border-color: rgba(220, 38, 38, 1);
transform: scale(1.05);
}
/* ============================================================================
Content
============================================================================ */
.conversation-embedding-panel__content {
flex: 1;
overflow: hidden;
position: relative;
}
.conversation-embedding-panel__loading,
.conversation-embedding-panel__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
height: 100%;
color: #666;
font-size: 0.875rem;
}
.conversation-embedding-panel__loading-icon {
animation: spin 1s linear infinite;
color: var(--primary-blue, #0a3dfa);
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.conversation-embedding-panel__empty svg {
color: #999;
}
.conversation-embedding-panel__empty p {
margin: 0;
text-align: center;
}
/* ============================================================================
Dark Theme
============================================================================ */
[data-theme="dark"] .conversation-embedding-panel {
background: var(--bg-primary-dark, #1a1a2e);
border-color: var(--border-color-dark, #333);
}
[data-theme="dark"] .conversation-embedding-panel__header {
background: linear-gradient(135deg, #1a4fff 0%, #3366ff 100%);
}
[data-theme="dark"] .conversation-embedding-panel__title {
color: white;
}
[data-theme="dark"] .conversation-embedding-panel__control-btn,
[data-theme="dark"] .conversation-embedding-panel__close-btn {
color: white;
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.25);
}
[data-theme="dark"] .conversation-embedding-panel__control-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.4);
}
[data-theme="dark"] .conversation-embedding-panel__close-btn:hover {
background: rgba(220, 38, 38, 0.9);
border-color: rgba(220, 38, 38, 1);
}
[data-theme="dark"] .conversation-embedding-panel__loading,
[data-theme="dark"] .conversation-embedding-panel__empty {
color: #999;
}
[data-theme="dark"] .conversation-embedding-panel__resize-handle--se::after {
border-color: rgba(255, 255, 255, 0.2);
}
[data-theme="dark"] .conversation-embedding-panel__resize-handle--se:hover::after {
border-color: rgba(100, 150, 255, 0.5);
}
/* ============================================================================
Responsive Adjustments
============================================================================ */
@media (max-width: 768px) {
.conversation-embedding-panel {
width: calc(100vw - 32px) !important;
max-width: 500px;
}
.conversation-embedding-panel__sources-badge {
display: none;
}
.conversation-embedding-panel__resize-handle {
display: none;
}
}
@media (max-width: 480px) {
.conversation-embedding-panel {
width: calc(100vw - 16px) !important;
left: 8px !important;
right: 8px !important;
transform: translateY(var(--panel-y, 0)) !important;
}
.conversation-embedding-panel__title {
font-size: 0.875rem;
}
.conversation-embedding-panel__control-btn,
.conversation-embedding-panel__close-btn {
width: 28px;
height: 28px;
}
}

View file

@ -0,0 +1,474 @@
/**
* ConversationEmbeddingPanel.tsx
*
* A draggable, floating panel that displays the Embedding Projector visualization.
* Shows embeddings retrieved during RAG queries for the current conversation.
*
* Features:
* - Draggable by header (matches InstitutionInfoPanel pattern)
* - Close button (X)
* - Pin button (keeps panel open during navigation)
* - Minimize button (collapses to header only)
* - Escape key to close
* - Stays within viewport bounds
* - Highlights RAG source embeddings
*
* Based on InstitutionInfoPanel.tsx drag pattern.
*/
import React, { useState, useRef, useEffect, useCallback, memo } from 'react';
import {
X,
Minimize2,
Maximize2,
Lock,
Unlock,
Layers,
Loader2,
Info,
} from 'lucide-react';
import { EmbeddingProjector, type EmbeddingPoint } from '../database/EmbeddingProjector';
import { isTargetInsideAny } from '../../utils/dom';
import './ConversationEmbeddingPanel.css';
// Panel dimensions - larger default for better readability
const PANEL_WIDTH = 700;
const PANEL_HEIGHT = 550;
const PANEL_HEIGHT_MINIMIZED = 48;
const PANEL_HEIGHT_SIMPLE = 450;
const PANEL_WIDTH_SIMPLE = 550;
const PANEL_MARGIN = 16;
const MIN_VISIBLE_WIDTH = 100;
const MIN_VISIBLE_HEIGHT = 100;
// Resize constraints
const MIN_WIDTH = 400;
const MIN_HEIGHT = 300;
const MAX_WIDTH = 1200;
const MAX_HEIGHT = 900;
// Resize direction type
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | null;
interface Position {
x: number;
y: number;
}
interface Size {
width: number;
height: number;
}
export interface ConversationEmbeddingPanelProps {
/** Embedding points to display */
points: EmbeddingPoint[];
/** Whether embeddings are currently loading */
isLoading?: boolean;
/** Indices of points to highlight (RAG sources) */
highlightedIndices?: number[];
/** Called when user closes the panel */
onClose: () => void;
/** Panel title */
title?: string;
/** Translation function - accepts any string key */
t: (key: string) => string;
/** Current language */
language: 'nl' | 'en';
/** Use simple/compact mode */
simpleMode?: boolean;
/** Whether panel is pinned */
isPinned?: boolean;
/** Called when pin state changes */
onTogglePin?: () => void;
/** Called when a context point is selected */
onContextSelect?: (point: EmbeddingPoint) => void;
/** Initial position (if not specified, defaults to bottom-right) */
initialPosition?: Position;
}
/**
* Draggable embedding projector panel for conversation page.
*/
const ConversationEmbeddingPanelComponent: React.FC<ConversationEmbeddingPanelProps> = ({
points,
isLoading = false,
highlightedIndices = [],
onClose,
title,
t,
language,
simpleMode = false,
isPinned = false,
onTogglePin,
onContextSelect,
initialPosition,
}) => {
const panelRef = useRef<HTMLDivElement>(null);
// Draggable state
const [position, setPosition] = useState<Position | null>(null);
const [isDragging, setIsDragging] = useState(false);
const dragStartRef = useRef<{ x: number; y: number; posX: number; posY: number } | null>(null);
// Resize state
const [size, setSize] = useState<Size | null>(null);
const [isResizing, setIsResizing] = useState(false);
const [resizeDirection, setResizeDirection] = useState<ResizeDirection>(null);
const resizeStartRef = useRef<{
x: number;
y: number;
width: number;
height: number;
posX: number;
posY: number;
} | null>(null);
// Minimize state
const [isMinimized, setIsMinimized] = useState(false);
// Track if user has manually positioned this panel
const hasUserPositioned = useRef(false);
// Calculate default panel dimensions based on mode
const defaultPanelWidth = simpleMode ? PANEL_WIDTH_SIMPLE : PANEL_WIDTH;
const defaultPanelHeight = simpleMode ? PANEL_HEIGHT_SIMPLE : PANEL_HEIGHT;
// Use user-resized dimensions or defaults
const panelWidth = size?.width ?? defaultPanelWidth;
const panelHeight = isMinimized ? PANEL_HEIGHT_MINIMIZED : (size?.height ?? defaultPanelHeight);
// Calculate initial position (bottom-right by default) and size
useEffect(() => {
// If position already exists (from dragging or previous render), keep it
if (position && hasUserPositioned.current) return;
// If we already have a position, don't recalculate
if (position) return;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Set initial size if not set
if (!size) {
setSize({ width: defaultPanelWidth, height: defaultPanelHeight });
}
let x: number;
let y: number;
if (initialPosition) {
x = initialPosition.x;
y = initialPosition.y;
} else {
// Default to bottom-right corner
x = viewportWidth - defaultPanelWidth - PANEL_MARGIN;
y = viewportHeight - defaultPanelHeight - PANEL_MARGIN;
}
// Clamp to viewport bounds
x = Math.max(PANEL_MARGIN, Math.min(x, viewportWidth - defaultPanelWidth - PANEL_MARGIN));
y = Math.max(PANEL_MARGIN, Math.min(y, viewportHeight - defaultPanelHeight - PANEL_MARGIN));
setPosition({ x, y });
}, [initialPosition, position, defaultPanelWidth, defaultPanelHeight, size]);
// Drag handlers
const handleMouseDown = useCallback((e: React.MouseEvent) => {
// Don't start drag if clicking on buttons or links
if (isTargetInsideAny(e.target, ['button', 'a', 'input', 'select'])) {
return;
}
e.preventDefault();
setIsDragging(true);
if (panelRef.current && position) {
dragStartRef.current = {
x: e.clientX,
y: e.clientY,
posX: position.x,
posY: position.y,
};
}
}, [position]);
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!isDragging || !dragStartRef.current) return;
const deltaX = e.clientX - dragStartRef.current.x;
const deltaY = e.clientY - dragStartRef.current.y;
const newX = dragStartRef.current.posX + deltaX;
const newY = dragStartRef.current.posY + deltaY;
// Clamp to viewport bounds (allow some overflow for edge docking)
const maxX = window.innerWidth - MIN_VISIBLE_WIDTH;
const maxY = window.innerHeight - MIN_VISIBLE_HEIGHT;
setPosition({
x: Math.max(-panelWidth + MIN_VISIBLE_WIDTH, Math.min(newX, maxX)),
y: Math.max(0, Math.min(newY, maxY)),
});
}, [isDragging, panelWidth]);
const handleMouseUp = useCallback(() => {
// Mark that user has positioned this panel
if (isDragging) {
hasUserPositioned.current = true;
}
setIsDragging(false);
dragStartRef.current = null;
}, [isDragging]);
// Global mouse event listeners for dragging
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
} else {
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isDragging, handleMouseMove, handleMouseUp]);
// Resize handlers
const handleResizeMouseDown = useCallback((direction: ResizeDirection) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
setResizeDirection(direction);
if (position && size) {
resizeStartRef.current = {
x: e.clientX,
y: e.clientY,
width: size.width,
height: size.height,
posX: position.x,
posY: position.y,
};
}
}, [position, size]);
const handleResizeMouseMove = useCallback((e: MouseEvent) => {
if (!isResizing || !resizeStartRef.current || !resizeDirection) return;
const deltaX = e.clientX - resizeStartRef.current.x;
const deltaY = e.clientY - resizeStartRef.current.y;
let newWidth = resizeStartRef.current.width;
let newHeight = resizeStartRef.current.height;
let newX = resizeStartRef.current.posX;
let newY = resizeStartRef.current.posY;
// Handle horizontal resize
if (resizeDirection.includes('e')) {
newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, resizeStartRef.current.width + deltaX));
}
if (resizeDirection.includes('w')) {
const potentialWidth = resizeStartRef.current.width - deltaX;
if (potentialWidth >= MIN_WIDTH && potentialWidth <= MAX_WIDTH) {
newWidth = potentialWidth;
newX = resizeStartRef.current.posX + deltaX;
}
}
// Handle vertical resize
if (resizeDirection.includes('s')) {
newHeight = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, resizeStartRef.current.height + deltaY));
}
if (resizeDirection.includes('n')) {
const potentialHeight = resizeStartRef.current.height - deltaY;
if (potentialHeight >= MIN_HEIGHT && potentialHeight <= MAX_HEIGHT) {
newHeight = potentialHeight;
newY = resizeStartRef.current.posY + deltaY;
}
}
setSize({ width: newWidth, height: newHeight });
// Update position if resizing from north or west
if (resizeDirection.includes('w') || resizeDirection.includes('n')) {
setPosition({ x: newX, y: newY });
}
}, [isResizing, resizeDirection]);
const handleResizeMouseUp = useCallback(() => {
setIsResizing(false);
setResizeDirection(null);
resizeStartRef.current = null;
}, []);
// Global mouse event listeners for resizing
useEffect(() => {
if (isResizing) {
window.addEventListener('mousemove', handleResizeMouseMove);
window.addEventListener('mouseup', handleResizeMouseUp);
document.body.style.userSelect = 'none';
// Set cursor based on direction
const cursorMap: Record<string, string> = {
n: 'ns-resize',
s: 'ns-resize',
e: 'ew-resize',
w: 'ew-resize',
ne: 'nesw-resize',
sw: 'nesw-resize',
nw: 'nwse-resize',
se: 'nwse-resize',
};
document.body.style.cursor = resizeDirection ? cursorMap[resizeDirection] : '';
}
return () => {
window.removeEventListener('mousemove', handleResizeMouseMove);
window.removeEventListener('mouseup', handleResizeMouseUp);
if (!isDragging) {
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
};
}, [isResizing, resizeDirection, handleResizeMouseMove, handleResizeMouseUp, isDragging]);
// Close on Escape key (only if not pinned)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isPinned) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose, isPinned]);
// Toggle minimize
const handleToggleMinimize = useCallback(() => {
setIsMinimized(prev => !prev);
}, []);
// Don't render if no position yet
if (!position) {
return null;
}
const displayTitle = title || t('embeddingProjector');
return (
<div
ref={panelRef}
className={`conversation-embedding-panel ${isMinimized ? 'conversation-embedding-panel--minimized' : ''} ${simpleMode ? 'conversation-embedding-panel--simple' : ''} ${isDragging ? 'conversation-embedding-panel--dragging' : ''} ${isResizing ? 'conversation-embedding-panel--resizing' : ''}`}
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
width: panelWidth,
height: panelHeight,
}}
>
{/* Resize handles (hidden when minimized) */}
{!isMinimized && (
<>
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--n" onMouseDown={handleResizeMouseDown('n')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--s" onMouseDown={handleResizeMouseDown('s')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--e" onMouseDown={handleResizeMouseDown('e')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--w" onMouseDown={handleResizeMouseDown('w')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--ne" onMouseDown={handleResizeMouseDown('ne')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--nw" onMouseDown={handleResizeMouseDown('nw')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--se" onMouseDown={handleResizeMouseDown('se')} />
<div className="conversation-embedding-panel__resize-handle conversation-embedding-panel__resize-handle--sw" onMouseDown={handleResizeMouseDown('sw')} />
</>
)}
{/* Draggable Header */}
<div
className="conversation-embedding-panel__header"
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
>
<h3 className="conversation-embedding-panel__title">
<Layers size={18} />
{displayTitle}
</h3>
<div className="conversation-embedding-panel__controls">
{/* Sources badge */}
{highlightedIndices.length > 0 && (
<span className="conversation-embedding-panel__sources-badge">
{highlightedIndices.length} {t('sourcesUsed')}
</span>
)}
{/* Pin button */}
{onTogglePin && (
<button
className={`conversation-embedding-panel__control-btn ${isPinned ? 'conversation-embedding-panel__control-btn--active' : ''}`}
onClick={onTogglePin}
title={isPinned ? (language === 'nl' ? 'Losmaken' : 'Unpin') : (language === 'nl' ? 'Vastzetten' : 'Pin')}
aria-label={isPinned ? 'Unpin' : 'Pin'}
>
{isPinned ? <Unlock size={18} strokeWidth={2.5} /> : <Lock size={18} strokeWidth={2.5} />}
</button>
)}
{/* Minimize button */}
<button
className="conversation-embedding-panel__control-btn"
onClick={handleToggleMinimize}
title={isMinimized ? (language === 'nl' ? 'Uitvouwen' : 'Expand') : (language === 'nl' ? 'Minimaliseren' : 'Minimize')}
aria-label={isMinimized ? 'Expand' : 'Minimize'}
>
{isMinimized ? <Maximize2 size={18} strokeWidth={2.5} /> : <Minimize2 size={18} strokeWidth={2.5} />}
</button>
{/* Close button */}
<button
className="conversation-embedding-panel__close-btn"
onClick={onClose}
title={language === 'nl' ? 'Sluiten' : 'Close'}
aria-label="Close"
>
<X size={18} strokeWidth={2.5} />
</button>
</div>
</div>
{/* Content (hidden when minimized) */}
{!isMinimized && (
<div className="conversation-embedding-panel__content">
{isLoading ? (
<div className="conversation-embedding-panel__loading">
<Loader2 className="conversation-embedding-panel__loading-icon" size={24} />
<span>{t('loadingEmbeddings')}</span>
</div>
) : points.length > 0 ? (
<EmbeddingProjector
points={points}
title={displayTitle}
simpleMode={simpleMode}
highlightedIndices={highlightedIndices}
showContextButton={true}
onContextSelect={onContextSelect}
/>
) : (
<div className="conversation-embedding-panel__empty">
<Info size={24} />
<p>{language === 'nl' ? 'Geen embeddings beschikbaar' : 'No embeddings available'}</p>
</div>
)}
</div>
)}
</div>
);
};
// Memoize to prevent unnecessary re-renders
export const ConversationEmbeddingPanel = memo(ConversationEmbeddingPanelComponent);
export default ConversationEmbeddingPanel;

View file

@ -24,3 +24,7 @@ export type { ConversationNetworkGraphProps } from './ConversationNetworkGraph';
export { ConversationSocialNetworkGraph, convertRetrievedResultsToGraph } from './ConversationSocialNetworkGraph';
export type { ConversationSocialNetworkGraphProps } from './ConversationSocialNetworkGraph';
// Draggable Embedding Projector panel
export { ConversationEmbeddingPanel } from './ConversationEmbeddingPanel';
export type { ConversationEmbeddingPanelProps } from './ConversationEmbeddingPanel';