diff --git a/client/src/api/interaction.api.ts b/client/src/api/interaction.api.ts
new file mode 100644
index 0000000..e7eed90
--- /dev/null
+++ b/client/src/api/interaction.api.ts
@@ -0,0 +1,23 @@
+import { EventLogBody } from "../types/interaction/interaction.types";
+import api from "./interceptor.api";
+
+export const createEventLog = async (eventlog: EventLogBody) => {
+ try {
+ const response = await api.post("/api/v1/event-logs/", eventlog);
+ return response;
+ } catch (error) {
+ console.error("Error creating event log:", error);
+ throw error;
+ }
+};
+
+export const fetchEventLogs = async () => {
+ try {
+ const response = await api.get(`/api/v1/event-logs/`, {
+ responseType: "blob",
+ });
+ return response;
+ } catch (error) {
+ throw error;
+ }
+};
diff --git a/client/src/components/sidebar/download-button/download-button.component.tsx b/client/src/components/sidebar/download-button/download-button.component.tsx
new file mode 100644
index 0000000..3ee9547
--- /dev/null
+++ b/client/src/components/sidebar/download-button/download-button.component.tsx
@@ -0,0 +1,40 @@
+import { Button, Typography, useTheme } from "@mui/material";
+import { getEventLogs } from "../../../services/interactions.service";
+import { Download } from "react-feather";
+
+const DownloadButton = () => {
+ const theme = useTheme();
+
+ const handleDownload = async () => {
+ try {
+ const response = await getEventLogs();
+
+ // Create a link element, set its href to the blob URL, and trigger a click to download the file
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "user_interaction_data.xlsx"); // The file name you want to save as
+ document.body.appendChild(link);
+ link.click();
+
+ // Clean up and remove the link
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error("Error downloading file:", error);
+ }
+ };
+
+ return (
+ }
+ sx={{ background: theme.palette.primary.dark, textTransform: "none" }}
+ onClick={handleDownload}
+ >
+ Download Data
+
+ );
+};
+
+export default DownloadButton;
diff --git a/client/src/components/sidebar/sidebar.component.tsx b/client/src/components/sidebar/sidebar.component.tsx
index a10bfab..72fb5d3 100644
--- a/client/src/components/sidebar/sidebar.component.tsx
+++ b/client/src/components/sidebar/sidebar.component.tsx
@@ -23,6 +23,8 @@ import {
import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { getUserPermissions } from "../../services/user.service";
+import { addEventLog } from "../../services/interactions.service";
+import DownloadButton from "./download-button/download-button.component";
function Sidebar({ open, setOpen }: SidebarParams) {
const navigate = useNavigate();
@@ -112,6 +114,17 @@ function Sidebar({ open, setOpen }: SidebarParams) {
}}
onClick={() => {
navigate(item.link);
+ if (
+ (import.meta.env.VITE_ENABLE_TRACKING as string) == "true"
+ ) {
+ if (item.name == "Insights") {
+ addEventLog({
+ location: item.name + " - Behavioral Indicators",
+ });
+ } else {
+ addEventLog({ location: item.name });
+ }
+ }
}}
>
);
})}
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default WordCloudGraph;
diff --git a/client/src/components/statistics/statistics.component.tsx b/client/src/components/statistics/statistics.component.tsx
index 48bb220..aec3d52 100644
--- a/client/src/components/statistics/statistics.component.tsx
+++ b/client/src/components/statistics/statistics.component.tsx
@@ -11,6 +11,7 @@ import { getPagesForInsights } from "../../services/pages.service";
import { getLabels } from "../../services/label.service";
import { getTags } from "../../services/tags.service";
import { useToolStore } from "../../states/global.store";
+import { addEventLog } from "../../services/interactions.service";
const Filter = lazy(() => import("./filter/filter.component"));
const BehavioralDashboard = lazy(
@@ -120,7 +121,14 @@ function Statistics({ open }: SidebarParams) {
key={tabItem.name}
elevation={0}
className="main tabs"
- onClick={() => setTab(tabItem.tab)}
+ onClick={() => {
+ setTab(tabItem.tab);
+ if ((import.meta.env.VITE_ENABLE_TRACKING as string) == "true") {
+ addEventLog({
+ location: "Insights - " + tabItem.name,
+ });
+ }
+ }}
sx={{
border: tabItem.tab === tab ? "solid 2px #7f7f7f" : 0,
}}
diff --git a/client/src/services/interactions.service.ts b/client/src/services/interactions.service.ts
new file mode 100644
index 0000000..803bd0f
--- /dev/null
+++ b/client/src/services/interactions.service.ts
@@ -0,0 +1,20 @@
+import { createEventLog, fetchEventLogs } from "../api/interaction.api";
+import { EventLogBody } from "../types/interaction/interaction.types";
+
+export const addEventLog = async (event: EventLogBody) => {
+ try {
+ const response = await createEventLog(event);
+ return response.data;
+ } catch (error: any) {
+ throw error;
+ }
+};
+
+export const getEventLogs = async () => {
+ try {
+ const response = await fetchEventLogs();
+ return response;
+ } catch (error: any) {
+ throw error;
+ }
+};
diff --git a/client/src/types/interaction/interaction.types.ts b/client/src/types/interaction/interaction.types.ts
new file mode 100644
index 0000000..a0b753e
--- /dev/null
+++ b/client/src/types/interaction/interaction.types.ts
@@ -0,0 +1,3 @@
+export type EventLogBody = {
+ location: string;
+};
diff --git a/server/chat/admin.py b/server/chat/admin.py
index 32a7c9d..562f774 100644
--- a/server/chat/admin.py
+++ b/server/chat/admin.py
@@ -1,7 +1,51 @@
from django.contrib import admin
from .models import Chat, Message, Label
+from django.contrib.auth.models import User
+import csv
+from django.http import HttpResponse
+from django.contrib import admin
+
+def export_as_csv(modeladmin, request, queryset):
+ meta = modeladmin.model._meta
+ field_names = [field.name for field in meta.fields]
+
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
+ writer = csv.writer(response)
+
+ writer.writerow(field_names)
+ for obj in queryset:
+ row = []
+ for field in field_names:
+ value = getattr(obj, field)
+ if isinstance(value, User): # Check if the field is a User instance
+ value = value.id # Replace the value with the user's ID
+ row.append(value)
+ writer.writerow(row)
+
+ return response
+
+export_as_csv.short_description = "Export Selected as CSV"
+
+class ModelsChatActions(admin.ModelAdmin):
+ list_display = [field.name for field in Chat._meta.fields]
+ actions = [export_as_csv]
+ search_fields = [field.name for field in Chat._meta.fields if field.name != 'id']
+ list_filter = [field.name for field in Chat._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]
+
+class ModelsMessagesActions(admin.ModelAdmin):
+ list_display = [field.name for field in Message._meta.fields]
+ actions = [export_as_csv]
+ search_fields = [field.name for field in Message._meta.fields if field.name != 'id']
+ list_filter = [field.name for field in Message._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]
+
+class ModelsLabelsActions(admin.ModelAdmin):
+ list_display = [field.name for field in Label._meta.fields]
+ actions = [export_as_csv]
+ search_fields = [field.name for field in Label._meta.fields if field.name != 'id']
+ list_filter = [field.name for field in Label._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]
# Register your models here.
-admin.site.register(Chat)
-admin.site.register(Message)
-admin.site.register(Label)
+admin.site.register(Chat, ModelsChatActions)
+admin.site.register(Message, ModelsMessagesActions)
+admin.site.register(Label, ModelsLabelsActions)
diff --git a/server/chat/views.py b/server/chat/views.py
index 4219c75..5c0a51f 100644
--- a/server/chat/views.py
+++ b/server/chat/views.py
@@ -127,6 +127,14 @@ def post(self, request, *args, **kwargs):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def delete(self, request, pk, format=None):
+ try:
+ label = Label.objects.get(id=pk)
+ except Label.DoesNotExist:
+ return Response(status=status.HTTP_404_NOT_FOUND)
+ label.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
class ChatByPageDetailView(APIView):
permission_classes = [IsAuthenticated]
diff --git a/server/interactions/__init__.py b/server/interactions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/server/interactions/admin.py b/server/interactions/admin.py
new file mode 100644
index 0000000..9ea8ee7
--- /dev/null
+++ b/server/interactions/admin.py
@@ -0,0 +1,37 @@
+from django.contrib import admin
+from .models import EventLog
+from django.contrib.auth.models import User
+import csv
+from django.http import HttpResponse
+from django.contrib import admin
+
+def export_as_csv(modeladmin, request, queryset):
+ meta = modeladmin.model._meta
+ field_names = [field.name for field in meta.fields]
+
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
+ writer = csv.writer(response)
+
+ writer.writerow(field_names)
+ for obj in queryset:
+ row = []
+ for field in field_names:
+ value = getattr(obj, field)
+ if isinstance(value, User): # Check if the field is a User instance
+ value = value.id # Replace the value with the user's ID
+ row.append(value)
+ writer.writerow(row)
+
+ return response
+
+export_as_csv.short_description = "Export Selected as CSV"
+
+class ModelsEventLogActions(admin.ModelAdmin):
+ list_display = [field.name for field in EventLog._meta.fields]
+ actions = [export_as_csv]
+ search_fields = [field.name for field in EventLog._meta.fields if field.name != 'id']
+ list_filter = [field.name for field in EventLog._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]
+
+# Register your models here.
+admin.site.register(EventLog, ModelsEventLogActions)
\ No newline at end of file
diff --git a/server/interactions/apps.py b/server/interactions/apps.py
new file mode 100644
index 0000000..67bf651
--- /dev/null
+++ b/server/interactions/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class InteractionsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'interactions'
diff --git a/server/interactions/migrations/0001_initial.py b/server/interactions/migrations/0001_initial.py
new file mode 100644
index 0000000..7cf12fc
--- /dev/null
+++ b/server/interactions/migrations/0001_initial.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.0.4 on 2024-08-26 10:30
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='EventLog',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('location', models.CharField(max_length=100)),
+ ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='eventlogs', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/server/interactions/migrations/0002_alter_eventlog_created_at.py b/server/interactions/migrations/0002_alter_eventlog_created_at.py
new file mode 100644
index 0000000..1c02f1a
--- /dev/null
+++ b/server/interactions/migrations/0002_alter_eventlog_created_at.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-08-26 13:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('interactions', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='eventlog',
+ name='created_at',
+ field=models.DateTimeField(auto_now_add=True),
+ ),
+ ]
diff --git a/server/interactions/migrations/__init__.py b/server/interactions/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/server/interactions/models.py b/server/interactions/models.py
new file mode 100644
index 0000000..5e96699
--- /dev/null
+++ b/server/interactions/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils import timezone
+
+class EventLog(models.Model):
+ location = models.CharField(max_length=100)
+ created_at = models.DateTimeField(auto_now_add=True)
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="eventlogs")
+
+ def __str__(self):
+ return self.location
\ No newline at end of file
diff --git a/server/interactions/serializers.py b/server/interactions/serializers.py
new file mode 100644
index 0000000..308f28d
--- /dev/null
+++ b/server/interactions/serializers.py
@@ -0,0 +1,9 @@
+from django.contrib.auth.models import User
+from rest_framework import serializers
+from .models import EventLog
+
+class EventLogSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = EventLog
+ fields = ["id", "location", "created_at"]
+ extra_kwargs = {"user": {"read_only": True}}
\ No newline at end of file
diff --git a/server/interactions/tests.py b/server/interactions/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/server/interactions/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/server/interactions/urls.py b/server/interactions/urls.py
new file mode 100644
index 0000000..3832f6d
--- /dev/null
+++ b/server/interactions/urls.py
@@ -0,0 +1,6 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path('event-logs/', views.EventLogView.as_view(), name='event_log'),
+]
diff --git a/server/interactions/views.py b/server/interactions/views.py
new file mode 100644
index 0000000..89d7b0a
--- /dev/null
+++ b/server/interactions/views.py
@@ -0,0 +1,48 @@
+import pandas as pd
+from django.shortcuts import render
+from rest_framework.views import APIView
+from rest_framework.permissions import IsAuthenticated
+from chat.models import Message, Chat
+from .models import EventLog
+from .serializers import EventLogSerializer
+from rest_framework.response import Response
+from django.http import HttpResponse
+from rest_framework import status
+
+class EventLogView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request, *args, **kwargs):
+ user = self.request.user
+ interaction_data = EventLog.objects.filter(user=user).values()
+ chat_data = Message.objects.filter(user=user).values('id', 'request', 'response', 'chat__title', 'chat__page__label', 'created_at')
+
+ # Convert the data to a pandas DataFrame
+ interaction_df = pd.DataFrame(list(interaction_data))
+ interaction_df['created_at'] = interaction_df['created_at'].astype(str)
+
+ chat_df = pd.DataFrame(list(chat_data))
+ chat_df['created_at'] = chat_df['created_at'].astype(str)
+
+ # Create an Excel file in memory
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = f'attachment; filename={user.username}_data.xlsx'
+
+ with pd.ExcelWriter(response, engine='openpyxl') as writer:
+ chat_df.to_excel(writer, index=False, sheet_name='ChatData')
+ interaction_df.to_excel(writer, index=False, sheet_name='InteractionData')
+
+ return response
+
+ def post(self, request, *args, **kwargs):
+ try:
+ data = {
+ 'location': request.data.get('location'),
+ }
+ except Exception:
+ return Response(status=status.HTTP_400_BAD_REQUEST)
+ serializer = EventLogSerializer(data=data)
+ if serializer.is_valid():
+ serializer.save(user=self.request.user)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
\ No newline at end of file
diff --git a/server/pages/admin.py b/server/pages/admin.py
index 7363d51..e1f28db 100644
--- a/server/pages/admin.py
+++ b/server/pages/admin.py
@@ -1,6 +1,45 @@
from django.contrib import admin
from .models import Tag, Page
+from django.contrib.auth.models import User
+import csv
+from django.http import HttpResponse
+from django.contrib import admin
+
+def export_as_csv(modeladmin, request, queryset):
+ meta = modeladmin.model._meta
+ field_names = [field.name for field in meta.fields]
+
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
+ writer = csv.writer(response)
+
+ writer.writerow(field_names)
+ for obj in queryset:
+ row = []
+ for field in field_names:
+ value = getattr(obj, field)
+ if isinstance(value, User): # Check if the field is a User instance
+ value = value.id # Replace the value with the user's ID
+ row.append(value)
+ writer.writerow(row)
+
+ return response
+
+export_as_csv.short_description = "Export Selected as CSV"
+
+class ModelsTagActions(admin.ModelAdmin):
+ list_display = [field.name for field in Tag._meta.fields]
+ actions = [export_as_csv]
+ search_fields = [field.name for field in Tag._meta.fields if field.name != 'id']
+ list_filter = [field.name for field in Tag._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]
+
+class ModelsPageActions(admin.ModelAdmin):
+ list_display = [field.name for field in Page._meta.fields]
+ actions = [export_as_csv]
+ search_fields = [field.name for field in Page._meta.fields if field.name != 'id']
+ list_filter = [field.name for field in Page._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]
+
# Register your models here.
-admin.site.register(Tag)
-admin.site.register(Page)
\ No newline at end of file
+admin.site.register(Tag, ModelsTagActions)
+admin.site.register(Page, ModelsPageActions)
\ No newline at end of file
diff --git a/server/pages/views.py b/server/pages/views.py
index 5d1ed1f..c152ca0 100644
--- a/server/pages/views.py
+++ b/server/pages/views.py
@@ -111,4 +111,12 @@ def post(self, request, *args, **kwargs):
if serializer.is_valid():
serializer.save(user=self.request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
\ No newline at end of file
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ def delete(self, request, pk, format=None):
+ try:
+ tag = Tag.objects.get(id=pk)
+ except Tag.DoesNotExist:
+ return Response(status=status.HTTP_404_NOT_FOUND)
+ tag.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
\ No newline at end of file
diff --git a/server/requirements.txt b/server/requirements.txt
index 19a98aa..287dfcd 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -14,4 +14,6 @@ gunicorn
whitenoise
exchangelib
nltk
-scikit-learn
\ No newline at end of file
+scikit-learn
+pandas
+openpyxl
\ No newline at end of file
diff --git a/server/server/settings.py b/server/server/settings.py
index 2ca9879..6b516a5 100644
--- a/server/server/settings.py
+++ b/server/server/settings.py
@@ -69,6 +69,7 @@
'pages',
'insights',
'deploy_management',
+ 'interactions',
'rest_framework',
'corsheaders'
]
diff --git a/server/server/urls.py b/server/server/urls.py
index eb2865b..dcabc43 100644
--- a/server/server/urls.py
+++ b/server/server/urls.py
@@ -14,4 +14,5 @@
path("api/v1/", include("pages.urls")),
path("api/v1/", include("users.urls")),
path("api/v1/", include("insights.urls")),
+ path("api/v1/", include("interactions.urls")),
]