253 lines
8.2 KiB
TypeScript
253 lines
8.2 KiB
TypeScript
/**
|
|
* ChatbotConversationsView
|
|
*
|
|
* Chatbot interface with chat history sidebar and messages view.
|
|
* Similar to trustee views but hardcoded for chatbot feature.
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { useChatbot } from '../../../hooks/useChatbot';
|
|
import { Messages } from '../../../components/UiComponents/Messages';
|
|
import { TextField } from '../../../components/UiComponents/TextField';
|
|
import { Button } from '../../../components/UiComponents/Button';
|
|
import { AutoScroll } from '../../../components/UiComponents/AutoScroll';
|
|
import { ChatMessage } from '../../../components/UiComponents/Messages/ChatMessages/ChatMessage';
|
|
import { Message } from '../../../components/UiComponents/Messages/MessagesTypes';
|
|
import { IoMdSend } from 'react-icons/io';
|
|
import { MdStop } from 'react-icons/md';
|
|
import { LuMessageSquare, LuTrash2 } from 'react-icons/lu';
|
|
import messagesStyles from '../../../components/UiComponents/Messages/Messages.module.css';
|
|
import styles from './ChatbotViews.module.css';
|
|
|
|
export const ChatbotConversationsView: React.FC = () => {
|
|
const {
|
|
threads,
|
|
selectedThreadId,
|
|
loadingThreads,
|
|
error,
|
|
messages,
|
|
loadingMessages,
|
|
isStreaming,
|
|
currentWorkflowId,
|
|
selectThread,
|
|
createNewThread,
|
|
sendMessage,
|
|
stopStreaming,
|
|
deleteThread,
|
|
refreshThreads,
|
|
inputValue,
|
|
setInputValue
|
|
} = useChatbot();
|
|
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!inputValue.trim() || isStreaming) return;
|
|
await sendMessage(inputValue);
|
|
};
|
|
|
|
const handleStop = async () => {
|
|
console.log('Stop button clicked', {
|
|
isStreaming,
|
|
currentWorkflowId,
|
|
selectedThreadId,
|
|
hasMessages: messages.length > 0
|
|
});
|
|
if (isStreaming) {
|
|
console.log('Calling stopStreaming...');
|
|
try {
|
|
await stopStreaming();
|
|
console.log('stopStreaming completed');
|
|
} catch (error) {
|
|
console.error('Error in stopStreaming:', error);
|
|
}
|
|
} else {
|
|
console.warn('Stop button clicked but not streaming');
|
|
}
|
|
};
|
|
|
|
const handleDeleteThread = async (e: React.MouseEvent, workflowId: string) => {
|
|
e.stopPropagation();
|
|
if (window.confirm('Möchten Sie diese Konversation wirklich löschen?')) {
|
|
setDeletingId(workflowId);
|
|
try {
|
|
await deleteThread(workflowId);
|
|
} finally {
|
|
setDeletingId(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
const formatDate = (timestamp?: number) => {
|
|
if (!timestamp) return '';
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return 'Gerade eben';
|
|
if (diffMins < 60) return `Vor ${diffMins} Min`;
|
|
if (diffHours < 24) return `Vor ${diffHours} Std`;
|
|
if (diffDays < 7) return `Vor ${diffDays} Tagen`;
|
|
|
|
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
};
|
|
|
|
const getThreadTitle = (thread: any) => {
|
|
if (thread.name) return thread.name;
|
|
// Try to get first message content as title
|
|
return 'Neue Konversation';
|
|
};
|
|
|
|
return (
|
|
<div className={styles.chatbotView}>
|
|
{/* Chat History Sidebar */}
|
|
<aside className={styles.chatHistory}>
|
|
<div className={styles.chatHistoryHeader}>
|
|
<h2 className={styles.chatHistoryTitle}>Konversationen</h2>
|
|
<button
|
|
className={styles.newChatButton}
|
|
onClick={createNewThread}
|
|
title="Neue Konversation"
|
|
>
|
|
<LuMessageSquare /> Neu
|
|
</button>
|
|
</div>
|
|
|
|
{loadingThreads ? (
|
|
<div className={styles.loading}>
|
|
<div className={styles.spinner} />
|
|
<span>Lade Konversationen...</span>
|
|
</div>
|
|
) : error ? (
|
|
<div className={styles.error}>
|
|
<p>{error}</p>
|
|
<button className={styles.retryButton} onClick={refreshThreads}>
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
) : threads.length === 0 ? (
|
|
<div className={styles.emptyState}>
|
|
<LuMessageSquare className={styles.emptyIcon} />
|
|
<p>Noch keine Konversationen vorhanden.</p>
|
|
<p className={styles.emptyHint}>Starte eine neue Konversation, um zu beginnen.</p>
|
|
</div>
|
|
) : (
|
|
<div className={styles.threadList}>
|
|
{threads.map((thread) => (
|
|
<div
|
|
key={thread.id}
|
|
className={`${styles.threadItem} ${selectedThreadId === thread.id ? styles.selected : ''}`}
|
|
onClick={() => selectThread(thread.id)}
|
|
>
|
|
<div className={styles.threadContent}>
|
|
<div className={styles.threadTitle}>{getThreadTitle(thread)}</div>
|
|
<div className={styles.threadMeta}>
|
|
{formatDate(thread.lastActivity || thread.startedAt)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
className={styles.deleteButton}
|
|
onClick={(e) => handleDeleteThread(e, thread.id)}
|
|
disabled={deletingId === thread.id}
|
|
title="Löschen"
|
|
>
|
|
{deletingId === thread.id ? (
|
|
<div className={styles.spinner} />
|
|
) : (
|
|
<LuTrash2 />
|
|
)}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</aside>
|
|
|
|
{/* Main Chat Area */}
|
|
<main className={styles.chatArea}>
|
|
{/* Messages Area */}
|
|
<div className={styles.messagesArea}>
|
|
{loadingMessages && messages.length === 0 ? (
|
|
<div className={styles.loading}>
|
|
<div className={styles.spinner} />
|
|
<span>Lade Nachrichten...</span>
|
|
</div>
|
|
) : messages.length === 0 ? (
|
|
<div className={`${messagesStyles.messagesContainer} ${messagesStyles.emptyContainer}`}>
|
|
<div className={messagesStyles.emptyState}>
|
|
Noch keine Nachrichten. Starte eine Konversation!
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<AutoScroll
|
|
scrollDependency={messages.length + (isStreaming ? 1 : 0)}
|
|
>
|
|
<div className={messagesStyles.messagesContainer}>
|
|
{messages.map((message) => (
|
|
<ChatMessage
|
|
key={message.id}
|
|
message={message}
|
|
showDocuments={true}
|
|
showMetadata={false}
|
|
showProgress={false}
|
|
/>
|
|
))}
|
|
{isStreaming && (
|
|
<div className={styles.typingIndicator}>
|
|
<div className={styles.typingBubble}>
|
|
<div className={styles.typingDots}>
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AutoScroll>
|
|
)}
|
|
</div>
|
|
|
|
{/* Input Form */}
|
|
<form onSubmit={handleSubmit} className={styles.inputForm}>
|
|
<TextField
|
|
value={inputValue}
|
|
onChange={setInputValue}
|
|
placeholder="Nachricht eingeben..."
|
|
disabled={isStreaming}
|
|
className={styles.inputField}
|
|
size="md"
|
|
/>
|
|
{isStreaming ? (
|
|
<Button
|
|
type="button"
|
|
onClick={handleStop}
|
|
variant="danger"
|
|
size="md"
|
|
icon={MdStop}
|
|
disabled={!isStreaming}
|
|
>
|
|
Stoppen
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="submit"
|
|
disabled={!inputValue.trim() || isStreaming}
|
|
variant="primary"
|
|
size="md"
|
|
icon={IoMdSend}
|
|
>
|
|
Senden
|
|
</Button>
|
|
)}
|
|
</form>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChatbotConversationsView;
|