Autosave: 20260208-230638

This commit is contained in:
Flatlogic Bot 2026-02-08 23:06:38 +00:00
parent 13a2f33af9
commit f9ad3d1087
29 changed files with 745 additions and 175 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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')

View File

@ -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')),
],
),
]

View 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),
),
]

View 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),
),
]

View 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'},
),
]

View 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')},
),
]

View File

@ -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})"

View 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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="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>
{% if last_sync_log %}
<small class="text-secondary mt-2">
Last sync: {{ last_sync_log.timestamp|timesince }} ago
{% if last_sync_log.status == 'success' %}
<span class="badge bg-success bg-opacity-10 text-success ms-1 border border-success border-opacity-25">Success</span>
{% else %}
<span class="badge bg-danger bg-opacity-10 text-danger ms-1 border border-danger border-opacity-25">Error</span>
{% endif %}
</small>
{% endif %}
</div> </div>
<div class="text-secondary small">
Logged in as <span class="text-light fw-bold">{{ user.username }}</span>
</div> </div>
</div> </div>
<!-- Search Section --> <div class="row mb-5">
<div class="glass-card p-4 mb-5"> <div class="col-12">
<form method="GET" action="{% url 'dashboard' %}" class="row g-3"> <div class="glass-card p-4">
<form method="get" class="row g-3">
<div class="col-md-10"> <div class="col-md-10">
<div class="input-group input-group-lg"> <div class="input-group input-group-lg bg-transparent">
<span class="input-group-text bg-transparent border-secondary text-secondary"> <span class="input-group-text bg-transparent border-end-0 text-secondary"><i class="bi bi-search"></i></span>
<i data-lucide="search"></i> <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 }}">
</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> </div>
<div class="col-md-2"> <div class="col-md-2">
<button type="submit" class="btn btn-cyan btn-lg w-100">Search</button> <button type="submit" class="btn btn-outline-info btn-lg w-100 fw-bold">Search</button>
</div> </div>
</form> </form>
</div> </div>
</div>
</div>
<!-- Results Section -->
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-lg-9">
{% if query %} {% if histories %}
<h5 class="mb-4 text-secondary">Search results for "{{ query }}"</h5> <div class="d-flex flex-column gap-3">
{% else %} {% for chat in histories %}
<h5 class="mb-4 text-secondary">Recent Conversations</h5> <div class="glass-card overflow-hidden">
{% endif %} <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">
{% if chats %} <h4 class="mb-1 fw-bold text-white">{{ chat.chat_title }}</h4>
<div class="row g-4"> <small class="text-secondary">{{ chat.chat_last_date|date:"M d, Y" }}</small>
{% for chat in chats %} </div>
<div class="col-12"> <p class="mb-3 text-secondary text-truncate opacity-75">{{ chat.chat_content|truncatewords:40 }}</p>
<div class="glass-card p-4"> <div class="d-flex align-items-center justify-content-between">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="bg-secondary bg-opacity-20 p-2 rounded-3 me-3"> <span class="badge rounded-pill bg-info bg-opacity-10 text-info border border-info border-opacity-25 me-3 px-3 py-2">
{% if chat.ai_chat_engine == 'ChatGPT' %} <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 }}
<i data-lucide="message-square" class="text-success"></i> </span>
{% elif chat.ai_chat_engine == 'Bing' %} {% if chat.ai_configuration %}
<i data-lucide="globe" class="text-info"></i> <small class="text-secondary"><i class="bi bi-key me-1"></i>{{ chat.ai_configuration.get_provider_display }}</small>
{% else %}
<i data-lucide="bot" class="text-warning"></i>
{% endif %} {% endif %}
</div> </div>
<div> <span class="text-info x-small fw-bold">VIEW DETAILS <i class="bi bi-chevron-right ms-1"></i></span>
<h5 class="m-0 fw-bold">{{ chat.chat_title }}</h5>
<span class="small text-secondary">{{ chat.ai_chat_engine }} &bull; {{ chat.chat_last_date|date:"M d, Y" }}</span>
</div> </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> </a>
</div> </div>
<div class="chat-content text-secondary">
{{ chat.chat_content|truncatewords:50|linebreaks }}
</div>
</div>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="text-center py-5"> <div class="glass-card text-center py-5">
<i data-lucide="inbox" class="text-secondary mb-3" size="48"></i> <i class="bi bi-chat-left-dots display-1 text-secondary opacity-25 mb-4"></i>
<p class="text-secondary">No chats found. Try a different search term or connect an engine.</p> <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> </div>
{% endif %} {% endif %}
</div> </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>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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>

View File

@ -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
View 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"

View File

@ -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')

View File

@ -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
View 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
View 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;
}