diff --git a/django_polly/admin.py b/django_polly/admin.py index 68f0a4e..0ff27f8 100644 --- a/django_polly/admin.py +++ b/django_polly/admin.py @@ -1,9 +1,11 @@ from django.contrib import admin -from .models import Parrot, Trick, Message, SmartConversation +from .models import Parrot, Trick, Message, SmartConversation, APIKey from django.utils.html import format_html from django.urls import reverse from django import forms +from django.contrib.auth import get_user_model +User = get_user_model() # Define a custom form for Parrot model class ParrotAdminForm(forms.ModelForm): @@ -26,6 +28,13 @@ class MessageInline(admin.TabularInline): ordering = ("created_at",) +class APIKeyInline(admin.TabularInline): + model = APIKey + extra = 0 + readonly_fields = ("key", "created_at") + can_delete = False + + @admin.register(Parrot) class ParrotAdmin(admin.ModelAdmin): list_display = ('name', 'color', 'age', 'external_id', 'created_at', 'updated_at') @@ -79,3 +88,16 @@ class MessageAdmin(admin.ModelAdmin): search_fields = ("content",) date_hierarchy = "created_at" raw_id_fields = ("conversation",) + + +@admin.register(APIKey) +class APIKeyAdmin(admin.ModelAdmin): + list_display = ("key", "user", "created_at") + list_filter = ("user", "created_at") + search_fields = ("key", "user__username") + readonly_fields = ("key", "created_at") + + +@admin.register(User) +class CustomUserAdmin(admin.ModelAdmin): + inlines = [APIKeyInline] diff --git a/django_polly/models.py b/django_polly/models.py index ddd87c5..23b6fbb 100644 --- a/django_polly/models.py +++ b/django_polly/models.py @@ -45,6 +45,15 @@ class ConversationParty(models.TextChoices): ASSISTANT = 'ASSISTANT', 'Assistant' +class APIKey(CommonFieldsModel): + key = models.CharField(max_length=255, unique=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_keys") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.key + + class SmartConversation(CommonFieldsModel): user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="conversations" diff --git a/django_polly/static/css/custom.css b/django_polly/static/css/custom.css index 4b8d4dc..73c8155 100644 --- a/django_polly/static/css/custom.css +++ b/django_polly/static/css/custom.css @@ -1 +1,50 @@ /* custom css goes here */ + +.fab { + position: fixed; + bottom: 20px; + right: 20px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 50%; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: background-color 0.3s ease; +} + +.fab:hover { + background-color: #4338ca; +} + +.chat-container { + display: none; + position: fixed; + bottom: 80px; + right: 20px; + width: 300px; + height: 400px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + overflow: hidden; + z-index: 1000; +} + +.chat-header { + background-color: #4f46e5; + color: white; + padding: 10px; + text-align: center; +} + +.chat-body { + padding: 10px; + height: calc(100% - 40px); + overflow-y: auto; +} diff --git a/django_polly/templates/conversation/iframe_chat.html b/django_polly/templates/conversation/iframe_chat.html new file mode 100644 index 0000000..4c88004 --- /dev/null +++ b/django_polly/templates/conversation/iframe_chat.html @@ -0,0 +1,176 @@ + + + + + + Smart Conversation + + + + + + +
💬
+
+
Chat
+
+
+
+ +
+ +
+
+ + +
+
+
+
+
+ + + + diff --git a/django_polly/templates/conversation/single_chat.html b/django_polly/templates/conversation/single_chat.html index c28332c..1789d08 100644 --- a/django_polly/templates/conversation/single_chat.html +++ b/django_polly/templates/conversation/single_chat.html @@ -117,4 +117,4 @@

{{ conversation_title }}

observer.observe(messageList, config); - \ No newline at end of file + diff --git a/django_polly/templates/example_iframe.html b/django_polly/templates/example_iframe.html new file mode 100644 index 0000000..6ec3826 --- /dev/null +++ b/django_polly/templates/example_iframe.html @@ -0,0 +1,12 @@ + + + + + + Example Iframe Chat + + +

Example Iframe Chat

+ + + diff --git a/django_polly/tests/test_views.py b/django_polly/tests/test_views.py index a558139..ce45664 100644 --- a/django_polly/tests/test_views.py +++ b/django_polly/tests/test_views.py @@ -31,3 +31,31 @@ def test_chat_view_with_unauthorized_user(self, client, conversation): client.force_login(unauthorized_user) response = client.get(reverse('django_polly:smart_gpt_chat', args=[conversation.id])) assert response.status_code == 403 + + +@pytest.mark.django_db +class TestIframeChatView: + @pytest.fixture + def user(self): + return User.objects.create_user(username='testuser', password='12345', is_staff=True, is_superuser=True) + + @pytest.fixture + def conversation(self, user): + return SmartConversation.objects.create(user=user, title="Test Chat") + + def test_iframe_chat_view_with_valid_api_key(self, client, user, conversation): + response = client.get(reverse('django_polly:iframe_chat'), {'api_key': 'your_api_key_here', 'conversation_id': conversation.id}) + assert response.status_code == 200 + assert 'conversation/single_chat.html' in [t.name for t in response.templates] + + def test_iframe_chat_view_with_invalid_api_key(self, client, user, conversation): + response = client.get(reverse('django_polly:iframe_chat'), {'api_key': 'invalid_key', 'conversation_id': conversation.id}) + assert response.status_code == 403 + + def test_iframe_chat_view_with_missing_conversation_id(self, client, user): + response = client.get(reverse('django_polly:iframe_chat'), {'api_key': 'your_api_key_here'}) + assert response.status_code == 404 + + def test_iframe_chat_view_with_nonexistent_conversation(self, client, user): + response = client.get(reverse('django_polly:iframe_chat'), {'api_key': 'your_api_key_here', 'conversation_id': 999}) + assert response.status_code == 404 diff --git a/django_polly/urls.py b/django_polly/urls.py index e5eeb95..5e429cd 100644 --- a/django_polly/urls.py +++ b/django_polly/urls.py @@ -1,4 +1,3 @@ - from django.urls import path from django_polly import views, consumers, consumers_admin from django_polly.views import DashboardView @@ -8,4 +7,5 @@ urlpatterns = [ path('dashboard/', DashboardView.as_view(), name='dashboard'), path('smart-gpt-chat//', views.smart_gpt_chat, name='smart_gpt_chat'), + path('iframe-chat/', views.iframe_chat, name='iframe_chat'), ] diff --git a/django_polly/views.py b/django_polly/views.py index dc481f5..c7b7ae1 100644 --- a/django_polly/views.py +++ b/django_polly/views.py @@ -3,8 +3,9 @@ from django.views.generic import TemplateView from django.shortcuts import render from django.template.response import TemplateResponse +from django.http import HttpResponseForbidden, HttpResponseNotFound -from .models import Parrot, Trick, SmartConversation +from .models import Parrot, Trick, SmartConversation, APIKey def smart_gpt_chat(request, conversation_id): @@ -19,6 +20,27 @@ def smart_gpt_chat(request, conversation_id): 'conversation_title': conversation.title}) +def iframe_chat(request): + api_key = request.GET.get('api_key') + conversation_id = request.GET.get('conversation_id') + + if not api_key or not conversation_id: + return HttpResponseForbidden("API key and conversation ID are required") + + try: + api_key_obj = APIKey.objects.get(key=api_key) + except APIKey.DoesNotExist: + return HttpResponseForbidden("Invalid API key") + + try: + conversation = SmartConversation.objects.get(id=conversation_id, user=api_key_obj.user) + except SmartConversation.DoesNotExist: + return HttpResponseNotFound("Conversation not found") + + return render(request, 'conversation/single_chat.html', {'conversation_id': conversation_id, + 'conversation_title': conversation.title}) + + class DashboardView(TemplateView): template_name = "django_polly/dashboard.html" diff --git a/docs/topics/consumers.rst b/docs/topics/consumers.rst index 37790ba..a75198a 100644 --- a/docs/topics/consumers.rst +++ b/docs/topics/consumers.rst @@ -39,4 +39,25 @@ Key Features: 2. Access to advanced LLM parameters 3. Ability to manage multiple conversations -For more details on implementing and customizing consumers, see the API reference. \ No newline at end of file +Iframe Access for ChatUI +------------------------ + +The ChatUI can now be accessed via an iframe using an API key. This feature allows secure embedding of the ChatUI in other applications. + +Key Features: +^^^^^^^^^^^^^ + +1. Secure access using API keys +2. Embeddable iframe for ChatUI +3. Validation of API key and conversation ID + +Usage Example: +^^^^^^^^^^^^^^ + +To access the ChatUI via an iframe, use the following URL format: + +.. code-block:: html + + + +For more details on implementing and customizing consumers, see the API reference.