chore(conversation-assistant): 🎨 Enhance conversation assistant studio with UI components (ScoreGauge, SignalsCard) and new features (bulk actions, search controls, auto-refresh).
This commit is contained in:
parent
c2d93e7b77
commit
29d86a5507
15 changed files with 89 additions and 61 deletions
|
|
@ -7,7 +7,7 @@ interface ScoreGaugeProps {
|
|||
inverse?: boolean;
|
||||
}
|
||||
|
||||
export function ScoreGauge({ value, label, inverse = false }: ScoreGaugeProps) {
|
||||
export const ScoreGauge = ({ value, label, inverse = false }: ScoreGaugeProps) => {
|
||||
// Clamp value between 0 and 100
|
||||
const clampedValue = Math.max(0, Math.min(100, value));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Zap, CheckCircle, XCircle, ChevronDown } from 'lucide-react';
|
||||
|
||||
import styles from './SignalsCard.module.css';
|
||||
|
||||
interface SignalsCardProps {
|
||||
|
|
@ -10,11 +12,11 @@ interface SignalsCardProps {
|
|||
|
||||
const MAX_VISIBLE = 5;
|
||||
|
||||
export function SignalsCard({
|
||||
export const SignalsCard = ({
|
||||
positiveSignals,
|
||||
negativeSignals,
|
||||
defaultExpanded = false,
|
||||
}: SignalsCardProps) {
|
||||
}: SignalsCardProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const [expandedPositive, setExpandedPositive] = useState(false);
|
||||
const [expandedNegative, setExpandedNegative] = useState(false);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ReactNode } from 'react';
|
||||
|
||||
import styles from './StudioLayout.module.css';
|
||||
|
||||
interface StudioLayoutProps {
|
||||
|
|
@ -6,11 +7,9 @@ interface StudioLayoutProps {
|
|||
analysisPanel: ReactNode;
|
||||
}
|
||||
|
||||
export function StudioLayout({ messagesPanel, analysisPanel }: StudioLayoutProps) {
|
||||
return (
|
||||
export const StudioLayout = ({ messagesPanel, analysisPanel }: StudioLayoutProps) => (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.messagesPane}>{messagesPanel}</div>
|
||||
<div className={styles.analysisPane}>{analysisPanel}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Lightbulb, ChevronDown } from 'lucide-react';
|
||||
|
||||
import styles from './SuggestedActionsCard.module.css';
|
||||
|
||||
interface SuggestedActionsCardProps {
|
||||
|
|
@ -7,10 +9,10 @@ interface SuggestedActionsCardProps {
|
|||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
export function SuggestedActionsCard({
|
||||
export const SuggestedActionsCard = ({
|
||||
actions,
|
||||
defaultExpanded = true,
|
||||
}: SuggestedActionsCardProps) {
|
||||
}: SuggestedActionsCardProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ComponentType, ReactNode } from 'react';
|
||||
import { bootstrap } from '@lilith/service-react-bootstrap';
|
||||
|
||||
import { AuthProvider } from '@lilith/auth-provider';
|
||||
import { bootstrap } from '@lilith/service-react-bootstrap';
|
||||
|
||||
import { App } from './App';
|
||||
import './index.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useParams, useNavigate } from '@lilith/ui-router';
|
||||
import { ArrowLeft, User, Clock, Bot } from 'lucide-react';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
import { Spinner } from '@lilith/ui-primitives';
|
||||
import { useParams, useNavigate } from '@lilith/ui-router';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import { ArrowLeft, User, Clock, Bot } from 'lucide-react';
|
||||
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
|
||||
import {
|
||||
useContact,
|
||||
useClassificationHistory,
|
||||
|
|
@ -286,7 +288,7 @@ const NoHistory = styled.p`
|
|||
margin: 0;
|
||||
`;
|
||||
|
||||
export function ContactDetailPage() {
|
||||
export const ContactDetailPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import type { ContactClassification } from '@/api';
|
||||
import { ClassificationSelector } from '@/components/ClassificationSelector';
|
||||
import {
|
||||
BulkActions,
|
||||
BulkButton,
|
||||
|
|
@ -8,6 +6,10 @@ import {
|
|||
SelectedCount,
|
||||
} from './ContactsPage.styles';
|
||||
|
||||
import type { ContactClassification } from '@/api';
|
||||
|
||||
import { ClassificationSelector } from '@/components/ClassificationSelector';
|
||||
|
||||
interface ContactsBulkActionsProps {
|
||||
selectedCount: number;
|
||||
showBulkAction: boolean;
|
||||
|
|
@ -16,13 +18,13 @@ interface ContactsBulkActionsProps {
|
|||
onBulkClassify: (classification: ContactClassification, reason?: string) => void;
|
||||
}
|
||||
|
||||
export function ContactsBulkActions({
|
||||
export const ContactsBulkActions = ({
|
||||
selectedCount,
|
||||
showBulkAction,
|
||||
isPending,
|
||||
onShowBulkAction,
|
||||
onBulkClassify,
|
||||
}: ContactsBulkActionsProps) {
|
||||
}: ContactsBulkActionsProps) => {
|
||||
if (selectedCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import styled from '@lilith/ui-styled-components';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
|
||||
export const Container = styled.div`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useNavigate, useSearchParams } from '@lilith/ui-router';
|
||||
|
||||
import { ContactsBulkActions } from './ContactsBulkActions';
|
||||
import { Container, Header, Title } from './ContactsPage.styles';
|
||||
import { validSortOptions, validSortOrders } from './ContactsPage.utils';
|
||||
import { ContactsSearchControls } from './ContactsSearchControls';
|
||||
import { ContactsTable } from './ContactsTable';
|
||||
|
||||
import {
|
||||
useContacts,
|
||||
useBulkClassifyContacts,
|
||||
|
|
@ -7,13 +15,8 @@ import {
|
|||
type ContactSortBy,
|
||||
type SortOrder,
|
||||
} from '@/api';
|
||||
import { ContactsBulkActions } from './ContactsBulkActions';
|
||||
import { ContactsSearchControls } from './ContactsSearchControls';
|
||||
import { ContactsTable } from './ContactsTable';
|
||||
import { Container, Header, Title } from './ContactsPage.styles';
|
||||
import { validSortOptions, validSortOrders } from './ContactsPage.utils';
|
||||
|
||||
export function ContactsPage() {
|
||||
export const ContactsPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
|
|
@ -37,10 +40,10 @@ export function ContactsPage() {
|
|||
// Sync state to URL
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (sortBy !== 'lastMessage') params.set('sortBy', sortBy);
|
||||
if (sortOrder !== 'desc') params.set('sortOrder', sortOrder);
|
||||
if (search) params.set('search', search);
|
||||
if (filter !== 'all') params.set('classification', filter);
|
||||
if (sortBy !== 'lastMessage') {params.set('sortBy', sortBy);}
|
||||
if (sortOrder !== 'desc') {params.set('sortOrder', sortOrder);}
|
||||
if (search) {params.set('search', search);}
|
||||
if (filter !== 'all') {params.set('classification', filter);}
|
||||
setSearchParams(params, { replace: true });
|
||||
}, [sortBy, sortOrder, search, filter, setSearchParams]);
|
||||
|
||||
|
|
@ -73,7 +76,7 @@ export function ContactsPage() {
|
|||
const bulkClassify = useBulkClassifyContacts();
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (!contacts) return;
|
||||
if (!contacts) {return;}
|
||||
if (selectedIds.size === contacts.length) {
|
||||
setSelectedIds(new Set());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { formatRelativeTime } from '@lilith/ui-utils';
|
||||
|
||||
import type { Contact, ContactClassification, ContactSortBy } from '@/api';
|
||||
|
||||
/**
|
||||
|
|
@ -70,7 +71,7 @@ export function getSortColumnHeader(sortBy: ContactSortBy): string {
|
|||
}
|
||||
}
|
||||
|
||||
export const classificationFilters: (ContactClassification | 'all')[] = [
|
||||
export const classificationFilters: Array<ContactClassification | 'all'> = [
|
||||
'all',
|
||||
'safe',
|
||||
'unknown',
|
||||
|
|
@ -79,7 +80,7 @@ export const classificationFilters: (ContactClassification | 'all')[] = [
|
|||
'confirmed-scam',
|
||||
];
|
||||
|
||||
export const sortOptions: { value: ContactSortBy; label: string }[] = [
|
||||
export const sortOptions: Array<{ value: ContactSortBy; label: string }> = [
|
||||
{ value: 'lastMessage', label: 'Last Message' },
|
||||
{ value: 'upcomingBirthday', label: 'Upcoming Birthday' },
|
||||
{ value: 'age', label: 'Age' },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ArrowUp, ArrowDown, MapPin } from 'lucide-react';
|
||||
import type { ContactClassification, ContactSortBy, SortOrder } from '@/api';
|
||||
import { ClassificationBadge } from '@/components/ClassificationBadge';
|
||||
|
||||
import {
|
||||
Controls,
|
||||
FilterButton,
|
||||
|
|
@ -15,6 +14,10 @@ import {
|
|||
} from './ContactsPage.styles';
|
||||
import { classificationFilters, sortOptions } from './ContactsPage.utils';
|
||||
|
||||
import type { ContactClassification, ContactSortBy, SortOrder } from '@/api';
|
||||
|
||||
import { ClassificationBadge } from '@/components/ClassificationBadge';
|
||||
|
||||
interface ContactsSearchControlsProps {
|
||||
search: string;
|
||||
filter: ContactClassification | 'all';
|
||||
|
|
@ -27,7 +30,7 @@ interface ContactsSearchControlsProps {
|
|||
onSortOrderChange: (sortOrder: SortOrder) => void;
|
||||
}
|
||||
|
||||
export function ContactsSearchControls({
|
||||
export const ContactsSearchControls = ({
|
||||
search,
|
||||
filter,
|
||||
sortBy,
|
||||
|
|
@ -37,8 +40,7 @@ export function ContactsSearchControls({
|
|||
onFilterChange,
|
||||
onSortByChange,
|
||||
onSortOrderChange,
|
||||
}: ContactsSearchControlsProps) {
|
||||
return (
|
||||
}: ContactsSearchControlsProps) => (
|
||||
<Controls>
|
||||
<SearchWrapper>
|
||||
<SearchIcon size={18} />
|
||||
|
|
@ -85,5 +87,4 @@ export function ContactsSearchControls({
|
|||
)}
|
||||
</SortControls>
|
||||
</Controls>
|
||||
);
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { User } from 'lucide-react';
|
||||
import { Spinner } from '@lilith/ui-primitives';
|
||||
import type { Contact, ContactSortBy } from '@/api';
|
||||
import { ClassificationBadge } from '@/components/ClassificationBadge';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Cell,
|
||||
|
|
@ -16,6 +15,11 @@ import {
|
|||
} from './ContactsPage.styles';
|
||||
import { formatSortValue, getSortColumnHeader } from './ContactsPage.utils';
|
||||
|
||||
import type { Contact, ContactSortBy } from '@/api';
|
||||
|
||||
import { ClassificationBadge } from '@/components/ClassificationBadge';
|
||||
|
||||
|
||||
interface ContactsTableProps {
|
||||
contacts: Contact[] | undefined;
|
||||
isLoading: boolean;
|
||||
|
|
@ -26,7 +30,7 @@ interface ContactsTableProps {
|
|||
onRowClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export function ContactsTable({
|
||||
export const ContactsTable = ({
|
||||
contacts,
|
||||
isLoading,
|
||||
selectedIds,
|
||||
|
|
@ -34,7 +38,7 @@ export function ContactsTable({
|
|||
onSelectAll,
|
||||
onSelect,
|
||||
onRowClick,
|
||||
}: ContactsTableProps) {
|
||||
}: ContactsTableProps) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import { useParams, Navigate, Link } from '@lilith/ui-router';
|
||||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Sparkles, Loader2, BrainCircuit } from 'lucide-react';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
|
||||
import { useToast } from '@lilith/ui-feedback';
|
||||
import { MessageBubble } from '@lilith/ui-messaging';
|
||||
import { Spinner } from '@lilith/ui-primitives';
|
||||
import { useToast } from '@lilith/ui-feedback';
|
||||
import { useParams, Navigate, Link } from '@lilith/ui-router';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import { Sparkles, Loader2, BrainCircuit } from 'lucide-react';
|
||||
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
|
||||
|
||||
import {
|
||||
useConversation,
|
||||
useMessagesInfinite,
|
||||
|
|
@ -192,7 +196,7 @@ function toUiMessageWithUrl(
|
|||
return uiMessage;
|
||||
}
|
||||
|
||||
export function ConversationDetailPage() {
|
||||
export const ConversationDetailPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const toast = useToast();
|
||||
const [selectedMessage, setSelectedMessage] = useState<string | null>(null);
|
||||
|
|
@ -232,7 +236,7 @@ export function ConversationDetailPage() {
|
|||
|
||||
// Flatten, sort, and transform messages (oldest first, newest at bottom)
|
||||
const sortedMessages = useMemo(() => {
|
||||
if (!messagesData?.pages) return [];
|
||||
if (!messagesData?.pages) {return [];}
|
||||
// Flatten all pages, then sort ascending by sentAt
|
||||
const allMessages = messagesData.pages.flatMap((page) => page.data);
|
||||
const sorted = [...allMessages].sort(
|
||||
|
|
@ -246,7 +250,7 @@ export function ConversationDetailPage() {
|
|||
|
||||
// Keep original messages for direction check (generate button)
|
||||
const originalMessages = useMemo(() => {
|
||||
if (!messagesData?.pages) return new Map<string, BackendMessage>();
|
||||
if (!messagesData?.pages) {return new Map<string, BackendMessage>();}
|
||||
const allMessages = messagesData.pages.flatMap((page) => page.data);
|
||||
return new Map(allMessages.map((msg) => [msg.id, msg as BackendMessage]));
|
||||
}, [messagesData]);
|
||||
|
|
@ -269,7 +273,7 @@ export function ConversationDetailPage() {
|
|||
|
||||
useEffect(() => {
|
||||
const element = loadMoreRef.current;
|
||||
if (!element) return;
|
||||
if (!element) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
root: messagesContainerRef.current,
|
||||
|
|
@ -283,7 +287,7 @@ export function ConversationDetailPage() {
|
|||
// Maintain scroll position when loading older messages
|
||||
useEffect(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
if (!container || isFetchingNextPage) return;
|
||||
if (!container || isFetchingNextPage) {return;}
|
||||
|
||||
if (isInitialLoad.current && sortedMessages.length > 0) {
|
||||
// Initial load: scroll to bottom
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Link } from '@lilith/ui-router';
|
||||
import { User, Users, ChevronRight } from 'lucide-react';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
|
||||
import { Spinner } from '@lilith/ui-primitives';
|
||||
import { Link } from '@lilith/ui-router';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import { formatRelativeTime } from '@lilith/ui-utils';
|
||||
import { User, Users, ChevronRight } from 'lucide-react';
|
||||
|
||||
import type { ThemeInterface } from '@lilith/ui-theme';
|
||||
|
||||
|
||||
import { useConversationsInfinite } from '@/api';
|
||||
|
||||
const Container = styled.div`
|
||||
|
|
@ -179,7 +183,7 @@ const LoadingMore = styled.div`
|
|||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export function ConversationsPage() {
|
||||
export const ConversationsPage = () => {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
|
@ -192,7 +196,7 @@ export function ConversationsPage() {
|
|||
|
||||
// Flatten pages into single array
|
||||
const conversations = useMemo(() => {
|
||||
if (!data?.pages) return [];
|
||||
if (!data?.pages) {return [];}
|
||||
return data.pages.flatMap((page) => page.data);
|
||||
}, [data]);
|
||||
|
||||
|
|
@ -211,7 +215,7 @@ export function ConversationsPage() {
|
|||
|
||||
useEffect(() => {
|
||||
const element = loadMoreRef.current;
|
||||
if (!element) return;
|
||||
if (!element) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
rootMargin: '200px', // Start loading before reaching the end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue