Autosave: 20260208-230638
This commit is contained in:
parent
13a2f33af9
commit
f9ad3d1087
Binary file not shown.
BIN
core/__pycache__/ai_service.cpython-311.pyc
Normal file
BIN
core/__pycache__/ai_service.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Company, Profile, AIChatHistory
|
from .models import Company, Profile, AIChatHistory, AIConfiguration, SyncHistoryLog
|
||||||
|
from .utils import encrypt_value, decrypt_value
|
||||||
|
|
||||||
@admin.register(Company)
|
@admin.register(Company)
|
||||||
class CompanyAdmin(admin.ModelAdmin):
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
@ -11,8 +12,25 @@ class ProfileAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('user', 'company', 'role')
|
list_display = ('user', 'company', 'role')
|
||||||
list_filter = ('company', 'role')
|
list_filter = ('company', 'role')
|
||||||
|
|
||||||
|
@admin.register(AIConfiguration)
|
||||||
|
class AIConfigurationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('company', 'provider', 'is_active', 'last_sync')
|
||||||
|
list_filter = ('provider', 'is_active')
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
# Encrypt API key before saving if it's changed or new
|
||||||
|
if 'api_key' in form.changed_data or not change:
|
||||||
|
obj.api_key = encrypt_value(obj.api_key)
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
@admin.register(AIChatHistory)
|
@admin.register(AIChatHistory)
|
||||||
class AIChatHistoryAdmin(admin.ModelAdmin):
|
class AIChatHistoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('chat_title', 'ai_chat_engine', 'company', 'chat_last_date')
|
list_display = ('chat_title', 'company', 'ai_chat_engine', 'chat_last_date')
|
||||||
list_filter = ('ai_chat_engine', 'company')
|
list_filter = ('company', 'ai_chat_engine')
|
||||||
search_fields = ('chat_title', 'chat_content')
|
search_fields = ('chat_title', 'chat_content')
|
||||||
|
|
||||||
|
@admin.register(SyncHistoryLog)
|
||||||
|
class SyncHistoryLogAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('timestamp', 'company', 'configuration', 'status', 'records_synced')
|
||||||
|
list_filter = ('status', 'company')
|
||||||
|
readonly_fields = ('timestamp', 'company', 'configuration', 'status', 'records_synced', 'error_message')
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 16:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AIConfiguration',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('provider', models.CharField(choices=[('openai', 'OpenAI'), ('perplexity', 'Perplexity')], max_length=50)),
|
||||||
|
('api_key', models.CharField(max_length=512)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('last_sync', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ai_configs', to='core.company')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='aichathistory',
|
||||||
|
name='ai_configuration',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_histories', to='core.aiconfiguration'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SyncHistoryLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('success', 'Success'), ('error', 'Error')], max_length=50)),
|
||||||
|
('records_synced', models.IntegerField(default=0)),
|
||||||
|
('error_message', models.TextField(blank=True, null=True)),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company')),
|
||||||
|
('configuration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.aiconfiguration')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0003_alter_aiconfiguration_provider.py
Normal file
18
core/migrations/0003_alter_aiconfiguration_provider.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 18:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_aiconfiguration_aichathistory_ai_configuration_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aiconfiguration',
|
||||||
|
name='provider',
|
||||||
|
field=models.CharField(choices=[('openai', 'OpenAI'), ('perplexity', 'Perplexity'), ('merlin', 'Merlin AI'), ('poe', 'POE')], max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0004_alter_aiconfiguration_provider.py
Normal file
18
core/migrations/0004_alter_aiconfiguration_provider.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 18:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_alter_aiconfiguration_provider'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aiconfiguration',
|
||||||
|
name='provider',
|
||||||
|
field=models.CharField(choices=[('openai', 'OpenAI'), ('perplexity', 'Perplexity'), ('merlin', 'Merlin AI'), ('poe', 'POE'), ('openrouter', 'OpenRouter (Free Tiers)')], max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
core/migrations/0005_alter_aichathistory_options.py
Normal file
17
core/migrations/0005_alter_aichathistory_options.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 19:20
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_alter_aiconfiguration_provider'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='aichathistory',
|
||||||
|
options={'ordering': ['-chat_last_date'], 'verbose_name_plural': 'AI Chat Histories'},
|
||||||
|
),
|
||||||
|
]
|
||||||
17
core/migrations/0006_alter_aichathistory_unique_together.py
Normal file
17
core/migrations/0006_alter_aichathistory_unique_together.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 19:29
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_alter_aichathistory_options'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='aichathistory',
|
||||||
|
unique_together={('company', 'ai_chat_id')},
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -17,8 +17,27 @@ class Profile(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username} ({self.company.name})"
|
return f"{self.user.username} ({self.company.name})"
|
||||||
|
|
||||||
|
class AIConfiguration(models.Model):
|
||||||
|
PROVIDER_CHOICES = [
|
||||||
|
('openai', 'OpenAI'),
|
||||||
|
('perplexity', 'Perplexity'),
|
||||||
|
('merlin', 'Merlin AI'),
|
||||||
|
('poe', 'POE'),
|
||||||
|
('openrouter', 'OpenRouter (Free Tiers)'),
|
||||||
|
]
|
||||||
|
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='ai_configs')
|
||||||
|
provider = models.CharField(max_length=50, choices=PROVIDER_CHOICES)
|
||||||
|
api_key = models.CharField(max_length=512) # Stored encrypted
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
last_sync = models.DateTimeField(null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.company.name} - {self.get_provider_display()}"
|
||||||
|
|
||||||
class AIChatHistory(models.Model):
|
class AIChatHistory(models.Model):
|
||||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='chat_histories')
|
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='chat_histories')
|
||||||
|
ai_configuration = models.ForeignKey(AIConfiguration, on_delete=models.SET_NULL, null=True, blank=True, related_name='chat_histories')
|
||||||
ai_chat_engine = models.CharField(max_length=100) # ChatGPT, Bing, etc.
|
ai_chat_engine = models.CharField(max_length=100) # ChatGPT, Bing, etc.
|
||||||
ai_chat_id = models.CharField(max_length=255)
|
ai_chat_id = models.CharField(max_length=255)
|
||||||
chat_title = models.CharField(max_length=512)
|
chat_title = models.CharField(max_length=512)
|
||||||
@ -28,10 +47,22 @@ class AIChatHistory(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "AI Chat Histories"
|
verbose_name_plural = "AI Chat Histories"
|
||||||
|
ordering = ['-chat_last_date']
|
||||||
|
unique_together = ('company', 'ai_chat_id')
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['chat_title']),
|
models.Index(fields=['chat_title']),
|
||||||
# We'll use database-level full-text search or icontains for now
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"[{self.ai_chat_engine}] {self.chat_title}"
|
return f"[{self.ai_chat_engine}] {self.chat_title}"
|
||||||
|
|
||||||
|
class SyncHistoryLog(models.Model):
|
||||||
|
company = models.ForeignKey(Company, on_delete=models.CASCADE)
|
||||||
|
configuration = models.ForeignKey(AIConfiguration, on_delete=models.CASCADE)
|
||||||
|
status = models.CharField(max_length=50, choices=[('success', 'Success'), ('error', 'Error')])
|
||||||
|
records_synced = models.IntegerField(default=0)
|
||||||
|
error_message = models.TextField(null=True, blank=True)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.timestamp} - {self.company.name} ({self.status})"
|
||||||
|
|||||||
15
core/templates/admin/base_site.html
Normal file
15
core/templates/admin/base_site.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | AI Chat Archive Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/admin_custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
<h1 id="site-name"><a href="{% url 'admin:index' %}">AI Chat Archive Admin</a></h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav-global %}{% endblock %}
|
||||||
@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
<!-- Bootstrap 5 CDN -->
|
<!-- Bootstrap 5 CDN -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
|
||||||
<!-- Lucide Icons -->
|
<!-- Lucide Icons -->
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
@ -80,6 +82,10 @@
|
|||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.x-small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
@ -122,7 +128,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{{ message.tags }} glass-card border-{{ message.tags }} mb-4">
|
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} glass-card border-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} mb-4 text-white">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@ -4,40 +4,45 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav aria-label="breadcrumb" class="mb-4">
|
<nav aria-label="breadcrumb" class="mb-5">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-info">Dashboard</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-info text-decoration-none"><i class="bi bi-house-door me-1"></i>Dashboard</a></li>
|
||||||
<li class="breadcrumb-item active text-secondary" aria-current="page">View Chat</li>
|
<li class="breadcrumb-item active text-secondary opacity-50" aria-current="page">View Chat</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="glass-card p-5">
|
<div class="glass-card p-4 p-md-5">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-5 pb-4 border-bottom border-secondary">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-start mb-5 pb-4 border-bottom border-secondary border-opacity-25">
|
||||||
<div>
|
<div class="mb-4 mb-md-0">
|
||||||
<span class="badge bg-secondary mb-2">{{ chat.ai_chat_engine }}</span>
|
<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 mb-3 px-3 py-2">
|
||||||
<h1 class="display-5 fw-bold">{{ chat.chat_title }}</h1>
|
<i class="bi {% if 'POE' in chat.ai_chat_engine %}bi-robot{% elif 'Merlin' in chat.ai_chat_engine %}bi-magic{% elif 'OPENROUTER' in chat.ai_chat_engine %}bi-share{% elif 'OpenAI' in chat.ai_chat_engine %}bi-openai{% else %}bi-cpu{% endif %} me-2"></i>{{ chat.ai_chat_engine }}
|
||||||
<p class="text-secondary m-0">Last updated: {{ chat.chat_last_date|date:"F j, Y, g:i a" }}</p>
|
</span>
|
||||||
|
<h1 class="display-5 fw-bold text-white mb-2">{{ chat.chat_title }}</h1>
|
||||||
|
<p class="text-secondary m-0"><i class="bi bi-clock me-2"></i>Last updated: {{ chat.chat_last_date|date:"F j, Y, g:i a" }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button class="btn btn-outline-info" onclick="copyToClipboard()">
|
<button class="btn btn-outline-info px-4" onclick="copyToClipboard()">
|
||||||
<i data-lucide="copy" size="18" class="me-2"></i>Copy Text
|
<i data-lucide="copy" size="18" class="me-2"></i>Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-viewport bg-black bg-opacity-20 p-4 rounded-3 border border-secondary shadow-inner">
|
<div class="chat-viewport bg-black bg-opacity-20 p-4 rounded-4 border border-secondary border-opacity-25 shadow-inner">
|
||||||
<div id="chat-body" class="chat-content text-light" style="white-space: pre-wrap; line-height: 1.6;">
|
<div id="chat-body" class="chat-content text-white opacity-75" style="white-space: pre-wrap; line-height: 1.8; letter-spacing: 0.01em;">
|
||||||
{{ chat.chat_content }}
|
{{ chat.chat_content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 d-flex gap-3">
|
<div class="mt-5 d-flex flex-wrap gap-3">
|
||||||
<button class="btn btn-cyan px-4">
|
<button class="btn btn-cyan px-4 py-2">
|
||||||
<i data-lucide="languages" size="18" class="me-2"></i>Translate
|
<i data-lucide="languages" size="18" class="me-2"></i>Translate Analysis
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary px-4">
|
<button class="btn btn-outline-secondary px-4 py-2 text-white">
|
||||||
<i data-lucide="link" size="18" class="me-2"></i>Open in {{ chat.ai_chat_engine }}
|
<i data-lucide="external-link" size="18" class="me-2"></i>View Source
|
||||||
</button>
|
</button>
|
||||||
|
<a href="{% url 'dashboard' %}" class="btn btn-link text-info text-decoration-none ms-md-auto">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>Back to Search
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,85 +1,151 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Dashboard - AI Chat Archive{% endblock %}
|
{% block title %}Dashboard - AI History Search{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="row mb-5">
|
||||||
<div class="d-flex justify-content-between align-items-end mb-5">
|
<div class="col-md-8">
|
||||||
<div>
|
<h1 class="fw-bold display-5">Search <span class="text-info">History</span></h1>
|
||||||
<span class="badge bg-info bg-opacity-10 text-info mb-2">{{ company.name }}</span>
|
<p class="text-secondary lead">Browse and search through your company's AI chat archives across all connected platforms.</p>
|
||||||
<h2 class="fw-bold m-0">Chat Archive</h2>
|
|
||||||
</div>
|
|
||||||
<div class="text-secondary small">
|
|
||||||
Logged in as <span class="text-light fw-bold">{{ user.username }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Section -->
|
|
||||||
<div class="glass-card p-4 mb-5">
|
|
||||||
<form method="GET" action="{% url 'dashboard' %}" class="row g-3">
|
|
||||||
<div class="col-md-10">
|
|
||||||
<div class="input-group input-group-lg">
|
|
||||||
<span class="input-group-text bg-transparent border-secondary text-secondary">
|
|
||||||
<i data-lucide="search"></i>
|
|
||||||
</span>
|
|
||||||
<input type="text" name="q" class="form-control bg-transparent border-secondary text-light"
|
|
||||||
placeholder="Search across all chats..." value="{{ query }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<button type="submit" class="btn btn-cyan btn-lg w-100">Search</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4 text-md-end d-flex align-items-center justify-content-md-end">
|
||||||
|
<div class="d-flex flex-column align-items-md-end">
|
||||||
|
<form action="{% url 'sync_history' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-cyan btn-lg shadow px-4 py-2">
|
||||||
|
<i class="bi bi-arrow-repeat me-2"></i>Sync Now
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
{% if last_sync_log %}
|
||||||
|
<small class="text-secondary mt-2">
|
||||||
<!-- Results Section -->
|
Last sync: {{ last_sync_log.timestamp|timesince }} ago
|
||||||
<div class="row">
|
{% if last_sync_log.status == 'success' %}
|
||||||
<div class="col-12">
|
<span class="badge bg-success bg-opacity-10 text-success ms-1 border border-success border-opacity-25">Success</span>
|
||||||
{% if query %}
|
|
||||||
<h5 class="mb-4 text-secondary">Search results for "{{ query }}"</h5>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<h5 class="mb-4 text-secondary">Recent Conversations</h5>
|
<span class="badge bg-danger bg-opacity-10 text-danger ms-1 border border-danger border-opacity-25">Error</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</small>
|
||||||
{% if chats %}
|
|
||||||
<div class="row g-4">
|
|
||||||
{% for chat in chats %}
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="glass-card p-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="bg-secondary bg-opacity-20 p-2 rounded-3 me-3">
|
|
||||||
{% if chat.ai_chat_engine == 'ChatGPT' %}
|
|
||||||
<i data-lucide="message-square" class="text-success"></i>
|
|
||||||
{% elif chat.ai_chat_engine == 'Bing' %}
|
|
||||||
<i data-lucide="globe" class="text-info"></i>
|
|
||||||
{% else %}
|
|
||||||
<i data-lucide="bot" class="text-warning"></i>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 class="m-0 fw-bold">{{ chat.chat_title }}</h5>
|
|
||||||
<span class="small text-secondary">{{ chat.ai_chat_engine }} • {{ chat.chat_last_date|date:"M d, Y" }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="{% url 'chat_detail' chat.pk %}" class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i data-lucide="external-link" size="16"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="chat-content text-secondary">
|
|
||||||
{{ chat.chat_content|truncatewords:50|linebreaks }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<i data-lucide="inbox" class="text-secondary mb-3" size="48"></i>
|
|
||||||
<p class="text-secondary">No chats found. Try a different search term or connect an engine.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="glass-card p-4">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="input-group input-group-lg bg-transparent">
|
||||||
|
<span class="input-group-text bg-transparent border-end-0 text-secondary"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" name="q" class="form-control bg-transparent border-start-0 ps-0 text-white" placeholder="Search by topic, keyword, or code snippet..." value="{{ query }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-outline-info btn-lg w-100 fw-bold">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-9">
|
||||||
|
{% if histories %}
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
{% for chat in histories %}
|
||||||
|
<div class="glass-card overflow-hidden">
|
||||||
|
<a href="{% url 'chat_detail' chat.pk %}" class="text-decoration-none p-4 d-block h-100">
|
||||||
|
<div class="d-flex w-100 justify-content-between mb-2">
|
||||||
|
<h4 class="mb-1 fw-bold text-white">{{ chat.chat_title }}</h4>
|
||||||
|
<small class="text-secondary">{{ chat.chat_last_date|date:"M d, Y" }}</small>
|
||||||
|
</div>
|
||||||
|
<p class="mb-3 text-secondary text-truncate opacity-75">{{ chat.chat_content|truncatewords:40 }}</p>
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="badge rounded-pill bg-info bg-opacity-10 text-info border border-info border-opacity-25 me-3 px-3 py-2">
|
||||||
|
<i class="bi {% if 'POE' in chat.ai_chat_engine %}bi-robot{% elif 'Merlin' in chat.ai_chat_engine %}bi-magic{% elif 'OPENROUTER' in chat.ai_chat_engine %}bi-share{% elif 'OpenAI' in chat.ai_chat_engine %}bi-openai{% else %}bi-cpu{% endif %} me-2"></i>{{ chat.ai_chat_engine }}
|
||||||
|
</span>
|
||||||
|
{% if chat.ai_configuration %}
|
||||||
|
<small class="text-secondary"><i class="bi bi-key me-1"></i>{{ chat.ai_configuration.get_provider_display }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="text-info x-small fw-bold">VIEW DETAILS <i class="bi bi-chevron-right ms-1"></i></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="glass-card text-center py-5">
|
||||||
|
<i class="bi bi-chat-left-dots display-1 text-secondary opacity-25 mb-4"></i>
|
||||||
|
<h3 class="text-white">No results found</h3>
|
||||||
|
<p class="text-secondary">Try adjusting your search query or sync new data from your AI providers.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="glass-card p-4 mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h5 class="fw-bold mb-0 text-white">AI Connections</h5>
|
||||||
|
<span class="badge bg-info bg-opacity-20 text-info">{{ ai_configs|length }}</span>
|
||||||
|
</div>
|
||||||
|
{% if ai_configs %}
|
||||||
|
{% for config in ai_configs %}
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<div class="flex-shrink-0 bg-secondary bg-opacity-10 p-2 rounded-3">
|
||||||
|
{% if config.provider == 'openai' %}
|
||||||
|
<i class="bi bi-openai fs-4 text-success"></i>
|
||||||
|
{% elif config.provider == 'merlin' %}
|
||||||
|
<i class="bi bi-magic fs-4 text-warning"></i>
|
||||||
|
{% elif config.provider == 'poe' %}
|
||||||
|
<i class="bi bi-robot fs-4 text-info"></i>
|
||||||
|
{% elif config.provider == 'openrouter' %}
|
||||||
|
<i class="bi bi-share fs-4 text-danger"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-lightning-charge fs-4 text-primary"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ms-3">
|
||||||
|
<div class="fw-bold text-white small">{{ config.get_provider_display }}</div>
|
||||||
|
<div class="text-secondary x-small">
|
||||||
|
{% if config.is_active %}
|
||||||
|
<span class="text-success"><i class="bi bi-circle-fill me-1" style="font-size: 6px;"></i> Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger"><i class="bi bi-circle me-1" style="font-size: 6px;"></i> Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-secondary small">No AI engines connected yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
<hr class="border-secondary opacity-25">
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="/admin/core/aiconfiguration/add/" class="btn btn-outline-light btn-sm py-2">Add Connection</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-card p-4 mb-4 border-top border-info border-3">
|
||||||
|
<h6 class="fw-bold mb-3 text-info"><i class="bi bi-gift me-2"></i>Free API Keys</h6>
|
||||||
|
<ul class="list-unstyled small mb-0 text-secondary">
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong class="text-white">OpenRouter:</strong> Offers free models like Llama 3.
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong class="text-white">Merlin AI:</strong> Sign up for a free tier access token.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="text-white">POE:</strong> Use your PB-Key from browser cookies.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-card p-4 bg-info bg-opacity-5">
|
||||||
|
<h6 class="fw-bold mb-2 text-white">Search Tip</h6>
|
||||||
|
<p class="small mb-0 text-secondary">Use specific keywords like <span class="text-info">"BGP"</span>, <span class="text-info">"Latency"</span>, or <span class="text-info">"Python"</span> to find solutions instantly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,38 +5,38 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="row min-vh-75 align-items-center">
|
<div class="row min-vh-75 align-items-center py-5">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<h1 class="display-3 fw-bold mb-4">
|
<h1 class="display-3 fw-bold mb-4 text-white">
|
||||||
Unlock Your <span class="text-info">Collective Intelligence</span>
|
Unlock Your <span class="text-info">Collective Intelligence</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="lead text-secondary mb-5">
|
<p class="lead text-secondary mb-5 opacity-75">
|
||||||
Retrieve, archive, and search through your entire AI chat history from ChatGPT, Bing, and more.
|
Retrieve, archive, and search through your entire AI chat history from ChatGPT, POE, Merlin, and more.
|
||||||
One unified search for all your past conversations.
|
One unified search for all your past conversations.
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3">
|
||||||
<a href="{% url 'login' %}" class="btn btn-cyan btn-lg px-5 py-3">Get Started</a>
|
<a href="{% url 'login' %}" class="btn btn-cyan btn-lg px-5 py-3 shadow">Get Started</a>
|
||||||
<a href="#how-it-works" class="btn btn-outline-light btn-lg px-5 py-3">How it Works</a>
|
<a href="#how-it-works" class="btn btn-outline-info btn-lg px-5 py-3">How it Works</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 mt-5 mt-lg-0">
|
<div class="col-lg-6 mt-5 mt-lg-0">
|
||||||
<div class="glass-card p-4 position-relative overflow-hidden">
|
<div class="glass-card p-4 p-md-5 position-relative overflow-hidden">
|
||||||
<div class="bg-info position-absolute top-0 end-0 p-5 rounded-circle" style="filter: blur(80px); opacity: 0.1;"></div>
|
<div class="bg-info position-absolute top-0 end-0 p-5 rounded-circle" style="filter: blur(80px); opacity: 0.1;"></div>
|
||||||
<div class="d-flex align-items-center mb-4">
|
<div class="d-flex align-items-center mb-4">
|
||||||
<div class="bg-info bg-opacity-10 p-2 rounded-3 me-3">
|
<div class="bg-info bg-opacity-10 p-3 rounded-4 me-4 border border-info border-opacity-25">
|
||||||
<i data-lucide="search" class="text-info"></i>
|
<i data-lucide="search" class="text-info" size="32"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<div class="bg-secondary bg-opacity-20 p-2 rounded w-75 mb-2"></div>
|
<div class="bg-secondary bg-opacity-20 p-2 rounded-pill w-75 mb-3"></div>
|
||||||
<div class="bg-secondary bg-opacity-10 p-2 rounded w-50"></div>
|
<div class="bg-secondary bg-opacity-10 p-2 rounded-pill w-50"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-top border-secondary pt-4">
|
<div class="border-top border-secondary border-opacity-25 pt-4">
|
||||||
<div class="chat-content text-secondary opacity-50">
|
<div class="chat-content text-secondary opacity-50">
|
||||||
> Retrieving histories...<br>
|
<span class="text-info">></span> Retrieving histories...<br>
|
||||||
> Found 142 chats from ChatGPT<br>
|
<span class="text-info">></span> Found 142 chats from ChatGPT<br>
|
||||||
> Found 89 chats from Perplexity<br>
|
<span class="text-info">></span> Found 89 chats from POE<br>
|
||||||
> Indexing for rapid search...
|
<span class="text-info">></span> Indexing for rapid search...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,62 +46,35 @@
|
|||||||
<!-- How It Works Section -->
|
<!-- How It Works Section -->
|
||||||
<div id="how-it-works" class="py-5 mt-5">
|
<div id="how-it-works" class="py-5 mt-5">
|
||||||
<div class="text-center mb-5">
|
<div class="text-center mb-5">
|
||||||
<h2 class="fw-bold h1">Three Steps to <span class="text-info">Knowledge Mastery</span></h2>
|
<h2 class="fw-bold display-5 text-white">Three Steps to <span class="text-info">Knowledge Mastery</span></h2>
|
||||||
<p class="text-secondary">Our platform bridges the gap between AI silos and your team's memory.</p>
|
<p class="text-secondary lead opacity-75">Our platform bridges the gap between AI silos and your team's memory.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
||||||
<div class="mb-3 d-flex align-items-center">
|
<div class="mb-3 d-flex align-items-center">
|
||||||
<span class="badge bg-info text-dark rounded-circle me-2 p-2" style="width:32px; height:32px; display:flex; align-items:center; justify-content:center;">1</span>
|
<span class="badge bg-info text-dark rounded-circle me-3 p-0 d-flex align-items-center justify-content-center" style="width:36px; height:36px; font-weight: bold;">1</span>
|
||||||
<h5 class="mb-0">Connect Engines</h5>
|
<h4 class="mb-0 text-white">Connect Engines</h4>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary mb-0">Use the <strong>"Connect AI"</strong> button in your dashboard to link your API keys securely. We support OpenAI, Anthropic, and Perplexity.</p>
|
<p class="text-secondary mb-0">Link your API keys securely in the dashboard. We support OpenAI, Merlin, POE, and OpenRouter.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
||||||
<div class="mb-3 d-flex align-items-center">
|
<div class="mb-3 d-flex align-items-center">
|
||||||
<span class="badge bg-info text-dark rounded-circle me-2 p-2" style="width:32px; height:32px; display:flex; align-items:center; justify-content:center;">2</span>
|
<span class="badge bg-info text-dark rounded-circle me-3 p-0 d-flex align-items-center justify-content-center" style="width:36px; height:36px; font-weight: bold;">2</span>
|
||||||
<h5 class="mb-0">Sync History</h5>
|
<h4 class="mb-0 text-white">Sync History</h4>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary mb-0">Click <strong>"Fetch Latest"</strong> to automatically download and index your recent conversations into your private company vault.</p>
|
<p class="text-secondary mb-0">Click <strong>"Sync Now"</strong> to automatically download and index your recent conversations into your private vault.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
||||||
<div class="mb-3 d-flex align-items-center">
|
<div class="mb-3 d-flex align-items-center">
|
||||||
<span class="badge bg-info text-dark rounded-circle me-2 p-2" style="width:32px; height:32px; display:flex; align-items:center; justify-content:center;">3</span>
|
<span class="badge bg-info text-dark rounded-circle me-3 p-0 d-flex align-items-center justify-content-center" style="width:36px; height:36px; font-weight: bold;">3</span>
|
||||||
<h5 class="mb-0">Search & Find</h5>
|
<h4 class="mb-0 text-white">Search & Find</h4>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary mb-0">Use the <strong>Search Bar</strong> to instantly find any past prompt or answer across all connected AI tools, including file attachments.</p>
|
<p class="text-secondary mb-0">Use the powerful search bar to instantly find any past prompt or answer across all connected AI tools.</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Features Grid -->
|
|
||||||
<div id="features" class="py-5 mt-5">
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="glass-card p-4 h-100">
|
|
||||||
<i data-lucide="database" class="text-info mb-3" size="32"></i>
|
|
||||||
<h4>Unified Storage</h4>
|
|
||||||
<p class="text-secondary">Centralize all your AI interactions in one secure, company-owned vault.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="glass-card p-4 h-100">
|
|
||||||
<i data-lucide="zap" class="text-info mb-3" size="32"></i>
|
|
||||||
<h4>Instant Search</h4>
|
|
||||||
<p class="text-secondary">Powerful keyword and similarity search powered by optimized database indexes.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="glass-card p-4 h-100">
|
|
||||||
<i data-lucide="shield" class="text-info mb-3" size="32"></i>
|
|
||||||
<h4>Multi-tenant</h4>
|
|
||||||
<p class="text-secondary">Enterprise-grade isolation ensuring your company's data stays private.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,4 +5,5 @@ urlpatterns = [
|
|||||||
path('', views.index, name='index'),
|
path('', views.index, name='index'),
|
||||||
path('dashboard/', views.dashboard, name='dashboard'),
|
path('dashboard/', views.dashboard, name='dashboard'),
|
||||||
path('chat/<int:pk>/', views.chat_detail, name='chat_detail'),
|
path('chat/<int:pk>/', views.chat_detail, name='chat_detail'),
|
||||||
|
path('sync-history/', views.sync_history, name='sync_history'),
|
||||||
]
|
]
|
||||||
34
core/utils.py
Normal file
34
core/utils.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from django.conf import settings
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
|
||||||
|
def get_fernet():
|
||||||
|
# Derive a key from Django's SECRET_KEY
|
||||||
|
password = settings.SECRET_KEY.encode()
|
||||||
|
salt = b'flatlogic_salt' # In production, this should be unique and stored
|
||||||
|
kdf = PBKDF2HMAC(
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
length=32,
|
||||||
|
salt=salt,
|
||||||
|
iterations=100000,
|
||||||
|
)
|
||||||
|
key = base64.urlsafe_b64encode(kdf.derive(password))
|
||||||
|
return Fernet(key)
|
||||||
|
|
||||||
|
def encrypt_value(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
f = get_fernet()
|
||||||
|
return f.encrypt(value.encode()).decode()
|
||||||
|
|
||||||
|
def decrypt_value(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
f = get_fernet()
|
||||||
|
try:
|
||||||
|
return f.decrypt(value.encode()).decode()
|
||||||
|
except Exception:
|
||||||
|
return "ERROR_DECRYPTING"
|
||||||
@ -1,7 +1,9 @@
|
|||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from .models import AIChatHistory, Profile
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.contrib import messages
|
||||||
|
from .models import AIChatHistory, AIConfiguration, SyncHistoryLog
|
||||||
|
from .ai_service import sync_ai_history
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
@ -10,25 +12,59 @@ def index(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
|
profile = request.user.profile
|
||||||
|
company = profile.company
|
||||||
|
|
||||||
query = request.GET.get('q', '')
|
query = request.GET.get('q', '')
|
||||||
profile = Profile.objects.get(user=request.user)
|
histories = AIChatHistory.objects.filter(company=company)
|
||||||
chats = AIChatHistory.objects.filter(company=profile.company)
|
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
chats = chats.filter(
|
histories = histories.filter(
|
||||||
Q(chat_title__icontains=query) | Q(chat_content__icontains=query)
|
Q(chat_title__icontains=query) |
|
||||||
).order_by('-chat_last_date')
|
Q(chat_content__icontains=query)
|
||||||
else:
|
)
|
||||||
chats = chats.order_by('-chat_last_date')[:10]
|
|
||||||
|
# Ordering is now handled by Model Meta
|
||||||
|
histories = histories.order_by('-chat_last_date')
|
||||||
|
|
||||||
|
# AI Configurations for the company
|
||||||
|
ai_configs = AIConfiguration.objects.filter(company=company)
|
||||||
|
last_sync_log = SyncHistoryLog.objects.filter(company=company).order_by('-timestamp').first()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'chats': chats,
|
'histories': histories,
|
||||||
'query': query,
|
'query': query,
|
||||||
'company': profile.company
|
'ai_configs': ai_configs,
|
||||||
|
'last_sync_log': last_sync_log,
|
||||||
}
|
}
|
||||||
return render(request, 'core/dashboard.html', context)
|
return render(request, 'core/dashboard.html', context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
def chat_detail(request, pk):
|
def chat_detail(request, pk):
|
||||||
profile = Profile.objects.get(user=request.user)
|
profile = request.user.profile
|
||||||
chat = AIChatHistory.objects.get(pk=pk, company=profile.company)
|
chat = get_object_or_404(AIChatHistory, pk=pk, company=profile.company)
|
||||||
return render(request, 'core/chat_detail.html', {'chat': chat})
|
return render(request, 'core/chat_detail.html', {'chat': chat})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def sync_history(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
profile = request.user.profile
|
||||||
|
configs = AIConfiguration.objects.filter(company=profile.company, is_active=True)
|
||||||
|
|
||||||
|
if not configs.exists():
|
||||||
|
messages.warning(request, "No active AI configurations found. Please contact your administrator to add an API key.")
|
||||||
|
else:
|
||||||
|
total_synced = 0
|
||||||
|
for config in configs:
|
||||||
|
sync_ai_history(config)
|
||||||
|
# Re-fetch the log we just created
|
||||||
|
latest_log = SyncHistoryLog.objects.filter(configuration=config).order_by('-timestamp').first()
|
||||||
|
if latest_log and latest_log.status == 'success':
|
||||||
|
total_synced += latest_log.records_synced
|
||||||
|
|
||||||
|
if total_synced > 0:
|
||||||
|
messages.success(request, f"Successfully synced {total_synced} new chat records.")
|
||||||
|
else:
|
||||||
|
messages.info(request, "Sync completed. No new records found.")
|
||||||
|
|
||||||
|
return redirect('dashboard')
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
Django==5.2.7
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
|
cryptography==42.0.5
|
||||||
|
httpx==0.27.0
|
||||||
53
setup_sync_demo.py
Normal file
53
setup_sync_demo.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from core.models import Company, Profile, AIConfiguration
|
||||||
|
from core.utils import encrypt_value
|
||||||
|
|
||||||
|
def setup_demo():
|
||||||
|
# 1. Get or create demo company
|
||||||
|
company, _ = Company.objects.get_or_create(
|
||||||
|
tenant_id='telecom-corp',
|
||||||
|
defaults={'name': 'Telecom Corp'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Get or create admin user
|
||||||
|
user = User.objects.filter(username='admin').first()
|
||||||
|
if not user:
|
||||||
|
user = User.objects.create_superuser('admin', 'admin@example.com', 'admin123')
|
||||||
|
|
||||||
|
# 3. Ensure user has a profile with the correct company
|
||||||
|
profile, created = Profile.objects.get_or_create(
|
||||||
|
user=user,
|
||||||
|
defaults={'company': company, 'role': 'admin'}
|
||||||
|
)
|
||||||
|
if not created and profile.company != company:
|
||||||
|
print(f"Updating {user.username}'s company from {profile.company.name} to {company.name}")
|
||||||
|
profile.company = company
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
# 4. Add AI Configurations
|
||||||
|
providers = [
|
||||||
|
('openai', 'sk-proj-demo-12345'),
|
||||||
|
('perplexity', 'pplx-demo-67890'),
|
||||||
|
('merlin', 'merlin-free-tier-key'),
|
||||||
|
('poe', 'pb-poe-access-token'),
|
||||||
|
('openrouter', 'sk-or-v1-free-key')
|
||||||
|
]
|
||||||
|
|
||||||
|
for provider, key in providers:
|
||||||
|
encrypted_key = encrypt_value(key)
|
||||||
|
AIConfiguration.objects.update_or_create(
|
||||||
|
company=company,
|
||||||
|
provider=provider,
|
||||||
|
defaults={'api_key': encrypted_key, 'is_active': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Demo setup complete. Added OpenAI, Perplexity, Merlin AI, POE, and OpenRouter configurations.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup_demo()
|
||||||
217
static/css/admin_custom.css
Normal file
217
static/css/admin_custom.css
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/* Custom styles for Django Admin to match the main site CI */
|
||||||
|
:root {
|
||||||
|
--primary-bg: #0f172a;
|
||||||
|
--accent-cyan: #22d3ee;
|
||||||
|
--text-main: #f1f5f9;
|
||||||
|
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--darker-bg: #020617;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--primary-bg) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
background: rgba(15, 23, 42, 0.8) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 15px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header a:link, #header a:visited {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#branding h1 {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.breadcrumbs {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
padding: 10px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.breadcrumbs a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module {
|
||||||
|
background: var(--glass-bg) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
margin-bottom: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module h2, .module caption, .inline-group h2 {
|
||||||
|
background: rgba(255, 255, 255, 0.03) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-main {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
padding: 20px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead th {
|
||||||
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td, table th {
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter {
|
||||||
|
background: rgba(15, 23, 42, 0.9) !important;
|
||||||
|
border-left: 1px solid var(--glass-border) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter h2 {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter a {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
border-left: 3px solid var(--accent-cyan) !important;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
||||||
|
background: var(--accent-cyan) !important;
|
||||||
|
color: var(--primary-bg) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
padding: 10px 20px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover, input[type=submit]:hover {
|
||||||
|
background: #06b6d4 !important;
|
||||||
|
box-shadow: 0 0 15px rgba(34, 211, 238, 0.4) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.default, input[type=submit].default {
|
||||||
|
background: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.closelink, .button.deletelink {
|
||||||
|
background: #ef4444 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned label {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text], input[type=password], input[type=email], input[type=url], input[type=number], textarea, select {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 0.4rem !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
border-color: var(--accent-cyan) !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 0.8rem !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.02) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-tools {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-tools a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar {
|
||||||
|
background: rgba(15, 23, 42, 0.95) !important;
|
||||||
|
border-right: 1px solid var(--glass-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .section th {
|
||||||
|
background: rgba(255, 255, 255, 0.03) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
padding: 12px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar a {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar tr.selected a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific fix for the login page */
|
||||||
|
.login body {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #container {
|
||||||
|
background: var(--glass-bg) !important;
|
||||||
|
backdrop-filter: blur(15px) !important;
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 1.5rem !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 450px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #header {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user