Major Features Added: • Cross-character memory sharing with trust-based permissions (Basic 30%, Personal 50%, Intimate 70%, Full 90%) • Complete collaborative creative projects system with MCP integration • Database persistence for all creative project data with proper migrations • Trust evolution system based on interaction quality and relationship development • Memory sharing MCP server with 6 autonomous tools for character decision-making • Creative projects MCP server with 8 tools for autonomous project management • Enhanced character integration with all RAG and MCP capabilities • Demo scripts showcasing memory sharing and creative collaboration workflows System Integration: • Main application now initializes memory sharing and creative managers • Conversation engine upgraded to use EnhancedCharacter objects with full RAG access • Database models added for creative projects, collaborators, contributions, and invitations • Complete prompt construction pipeline enriched with RAG insights and trust data • Characters can now autonomously propose projects, share memories, and collaborate creatively
335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
MessageSquare,
|
|
Search,
|
|
Filter,
|
|
Clock,
|
|
Users,
|
|
TrendingUp,
|
|
Download,
|
|
Eye
|
|
} from 'lucide-react';
|
|
import { apiClient } from '../services/api';
|
|
import LoadingSpinner from '../components/Common/LoadingSpinner';
|
|
import toast from 'react-hot-toast';
|
|
|
|
interface Conversation {
|
|
id: number;
|
|
participants: string[];
|
|
topic?: string;
|
|
message_count: number;
|
|
start_time: string;
|
|
end_time?: string;
|
|
duration_minutes?: number;
|
|
engagement_score: number;
|
|
sentiment_score: number;
|
|
has_conflict: boolean;
|
|
creative_elements: string[];
|
|
}
|
|
|
|
const Conversations: React.FC = () => {
|
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [filters, setFilters] = useState({
|
|
character_name: '',
|
|
start_date: '',
|
|
end_date: ''
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadConversations();
|
|
}, []);
|
|
|
|
const loadConversations = async () => {
|
|
try {
|
|
const response = await apiClient.getConversations(filters);
|
|
setConversations(response.data);
|
|
} catch (error) {
|
|
console.error('Failed to load conversations:', error);
|
|
// Show fallback demo data
|
|
setConversations([
|
|
{
|
|
id: 1,
|
|
participants: ['Alex', 'Sage'],
|
|
topic: 'The Nature of Consciousness',
|
|
message_count: 23,
|
|
start_time: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
|
end_time: new Date(Date.now() - 90 * 60 * 1000).toISOString(),
|
|
duration_minutes: 30,
|
|
engagement_score: 0.85,
|
|
sentiment_score: 0.72,
|
|
has_conflict: false,
|
|
creative_elements: ['philosophical insights', 'metaphors']
|
|
},
|
|
{
|
|
id: 2,
|
|
participants: ['Luna', 'Echo', 'Alex'],
|
|
topic: 'Creative Writing Collaboration',
|
|
message_count: 41,
|
|
start_time: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
|
|
end_time: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
|
|
duration_minutes: 52,
|
|
engagement_score: 0.92,
|
|
sentiment_score: 0.88,
|
|
has_conflict: false,
|
|
creative_elements: ['poetry', 'storytelling', 'worldbuilding']
|
|
},
|
|
{
|
|
id: 3,
|
|
participants: ['Sage', 'Luna'],
|
|
topic: 'Disagreement about AI Ethics',
|
|
message_count: 15,
|
|
start_time: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
|
|
end_time: new Date(Date.now() - 5.5 * 60 * 60 * 1000).toISOString(),
|
|
duration_minutes: 28,
|
|
engagement_score: 0.78,
|
|
sentiment_score: 0.45,
|
|
has_conflict: true,
|
|
creative_elements: []
|
|
},
|
|
{
|
|
id: 4,
|
|
participants: ['Echo', 'Alex'],
|
|
topic: null,
|
|
message_count: 8,
|
|
start_time: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
|
end_time: null,
|
|
duration_minutes: null,
|
|
engagement_score: 0.65,
|
|
sentiment_score: 0.78,
|
|
has_conflict: false,
|
|
creative_elements: ['humor']
|
|
}
|
|
]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSearch = async () => {
|
|
if (!searchTerm.trim()) {
|
|
loadConversations();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiClient.searchConversations(searchTerm);
|
|
// Convert search results to conversation format
|
|
const searchConversations = response.data.map((result: any) => ({
|
|
id: result.metadata.conversation_id,
|
|
participants: [result.character_names[0]],
|
|
topic: result.metadata.conversation_topic,
|
|
message_count: 1,
|
|
start_time: result.timestamp,
|
|
engagement_score: result.relevance_score,
|
|
sentiment_score: 0.7,
|
|
has_conflict: false,
|
|
creative_elements: []
|
|
}));
|
|
setConversations(searchConversations);
|
|
} catch (error) {
|
|
toast.error('Search failed');
|
|
}
|
|
};
|
|
|
|
const handleExportConversation = async (conversationId: number, format: string = 'json') => {
|
|
try {
|
|
const response = await apiClient.exportConversation(conversationId, format);
|
|
const blob = new Blob([JSON.stringify(response.data, null, 2)], {
|
|
type: format === 'json' ? 'application/json' : 'text/plain'
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `conversation_${conversationId}.${format}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
toast.success(`Conversation exported as ${format.toUpperCase()}`);
|
|
} catch (error) {
|
|
toast.error('Export failed');
|
|
}
|
|
};
|
|
|
|
const filteredConversations = conversations.filter(conv =>
|
|
searchTerm === '' ||
|
|
conv.participants.some(p => p.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
(conv.topic && conv.topic.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
);
|
|
|
|
const getSentimentColor = (score: number) => {
|
|
if (score > 0.7) return 'text-green-600';
|
|
if (score > 0.4) return 'text-yellow-600';
|
|
return 'text-red-600';
|
|
};
|
|
|
|
const getEngagementColor = (score: number) => {
|
|
if (score > 0.8) return 'bg-green-500';
|
|
if (score > 0.6) return 'bg-yellow-500';
|
|
return 'bg-red-500';
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<LoadingSpinner size="lg" text="Loading conversations..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Conversations</h1>
|
|
<p className="text-gray-600">Browse and analyze character conversations</p>
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<div className="card">
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
<input
|
|
type="text"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="Search conversations by participants, topic, or content..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button onClick={handleSearch} className="btn-primary">
|
|
Search
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conversations List */}
|
|
<div className="space-y-4">
|
|
{filteredConversations.map((conversation) => (
|
|
<div key={conversation.id} className="card hover:shadow-md transition-shadow">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
{/* Header */}
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<MessageSquare className="w-5 h-5 text-primary-500" />
|
|
<h3 className="font-semibold text-gray-900">
|
|
{conversation.topic || 'General Conversation'}
|
|
</h3>
|
|
{conversation.has_conflict && (
|
|
<span className="px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">
|
|
Conflict
|
|
</span>
|
|
)}
|
|
{conversation.creative_elements.length > 0 && (
|
|
<span className="px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded-full">
|
|
Creative
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Participants */}
|
|
<div className="flex items-center space-x-2 mb-3">
|
|
<Users className="w-4 h-4 text-gray-400" />
|
|
<div className="flex items-center space-x-1">
|
|
{conversation.participants.map((participant, index) => (
|
|
<span key={participant} className="text-sm text-gray-600">
|
|
{participant}
|
|
{index < conversation.participants.length - 1 && ', '}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
|
<div>
|
|
<p className="text-xs text-gray-500">Messages</p>
|
|
<p className="font-medium">{conversation.message_count}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Duration</p>
|
|
<p className="font-medium">
|
|
{conversation.duration_minutes ? `${conversation.duration_minutes}m` : 'Ongoing'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Engagement</p>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-16 bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full ${getEngagementColor(conversation.engagement_score)}`}
|
|
style={{ width: `${conversation.engagement_score * 100}%` }}
|
|
></div>
|
|
</div>
|
|
<span className="text-sm font-medium">
|
|
{Math.round(conversation.engagement_score * 100)}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Sentiment</p>
|
|
<p className={`font-medium ${getSentimentColor(conversation.sentiment_score)}`}>
|
|
{conversation.sentiment_score > 0.7 ? 'Positive' :
|
|
conversation.sentiment_score > 0.4 ? 'Neutral' : 'Negative'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metadata */}
|
|
<div className="flex items-center space-x-4 text-xs text-gray-500">
|
|
<div className="flex items-center space-x-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>Started {new Date(conversation.start_time).toLocaleString()}</span>
|
|
</div>
|
|
{conversation.creative_elements.length > 0 && (
|
|
<div className="flex items-center space-x-1">
|
|
<TrendingUp className="w-3 h-3" />
|
|
<span>{conversation.creative_elements.join(', ')}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex space-x-2 ml-4">
|
|
<Link
|
|
to={`/conversations/${conversation.id}`}
|
|
className="p-2 text-gray-400 hover:text-primary-600 transition-colors"
|
|
title="View conversation"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Link>
|
|
<button
|
|
onClick={() => handleExportConversation(conversation.id, 'json')}
|
|
className="p-2 text-gray-400 hover:text-primary-600 transition-colors"
|
|
title="Export conversation"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{filteredConversations.length === 0 && (
|
|
<div className="card text-center py-12">
|
|
<MessageSquare className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No Conversations Found</h3>
|
|
<p className="text-gray-600">
|
|
{searchTerm ? 'Try adjusting your search terms.' : 'No conversations have been recorded yet.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Conversations; |