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 .models import Company, Profile, AIChatHistory
from .models import Company, Profile, AIChatHistory, AIConfiguration, SyncHistoryLog
from .utils import encrypt_value, decrypt_value
@admin.register(Company)
class CompanyAdmin(admin.ModelAdmin):
@ -11,8 +12,25 @@ class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', '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)
class AIChatHistoryAdmin(admin.ModelAdmin):
list_display = ('chat_title', 'ai_chat_engine', 'company', 'chat_last_date')
list_filter = ('ai_chat_engine', 'company')
search_fields = ('chat_title', 'chat_content')
list_display = ('chat_title', 'company', 'ai_chat_engine', 'chat_last_date')
list_filter = ('company', 'ai_chat_engine')
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):
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):
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_id = models.CharField(max_length=255)
chat_title = models.CharField(max_length=512)
@ -28,10 +47,22 @@ class AIChatHistory(models.Model):
class Meta:
verbose_name_plural = "AI Chat Histories"
ordering = ['-chat_last_date']
unique_together = ('company', 'ai_chat_id')
indexes = [
models.Index(fields=['chat_title']),
# We'll use database-level full-text search or icontains for now
]
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 -->
<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 -->
<script src="https://unpkg.com/lucide@latest"></script>
@ -80,6 +82,10 @@
padding: 2px 4px;
border-radius: 4px;
}
.x-small {
font-size: 0.75rem;
}
</style>
{% block head %}{% endblock %}
</head>
@ -122,7 +128,7 @@
<div class="container">
{% if 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 }}
</div>
{% endfor %}
@ -144,4 +150,4 @@
</script>
{% block scripts %}{% endblock %}
</body>
</html>
</html>

View File

@ -4,40 +4,45 @@
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="mb-4">
<nav aria-label="breadcrumb" class="mb-5">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-info">Dashboard</a></li>
<li class="breadcrumb-item active text-secondary" aria-current="page">View Chat</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 opacity-50" aria-current="page">View Chat</li>
</ol>
</nav>
<div class="glass-card p-5">
<div class="d-flex justify-content-between align-items-start mb-5 pb-4 border-bottom border-secondary">
<div>
<span class="badge bg-secondary mb-2">{{ chat.ai_chat_engine }}</span>
<h1 class="display-5 fw-bold">{{ chat.chat_title }}</h1>
<p class="text-secondary m-0">Last updated: {{ chat.chat_last_date|date:"F j, Y, g:i a" }}</p>
<div class="glass-card p-4 p-md-5">
<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 class="mb-4 mb-md-0">
<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 mb-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>
<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 class="d-flex gap-2">
<button class="btn btn-outline-info" onclick="copyToClipboard()">
<i data-lucide="copy" size="18" class="me-2"></i>Copy Text
<button class="btn btn-outline-info px-4" onclick="copyToClipboard()">
<i data-lucide="copy" size="18" class="me-2"></i>Copy
</button>
</div>
</div>
<div class="chat-viewport bg-black bg-opacity-20 p-4 rounded-3 border border-secondary shadow-inner">
<div id="chat-body" class="chat-content text-light" style="white-space: pre-wrap; line-height: 1.6;">
<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-white opacity-75" style="white-space: pre-wrap; line-height: 1.8; letter-spacing: 0.01em;">
{{ chat.chat_content }}
</div>
</div>
<div class="mt-5 d-flex gap-3">
<button class="btn btn-cyan px-4">
<i data-lucide="languages" size="18" class="me-2"></i>Translate
<div class="mt-5 d-flex flex-wrap gap-3">
<button class="btn btn-cyan px-4 py-2">
<i data-lucide="languages" size="18" class="me-2"></i>Translate Analysis
</button>
<button class="btn btn-outline-secondary px-4">
<i data-lucide="link" size="18" class="me-2"></i>Open in {{ chat.ai_chat_engine }}
<button class="btn btn-outline-secondary px-4 py-2 text-white">
<i data-lucide="external-link" size="18" class="me-2"></i>View Source
</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>
@ -52,4 +57,4 @@
});
}
</script>
{% endblock %}
{% endblock %}

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 %}
<div class="container">
<div class="d-flex justify-content-between align-items-end mb-5">
<div>
<span class="badge bg-info bg-opacity-10 text-info mb-2">{{ company.name }}</span>
<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 class="row mb-5">
<div class="col-md-8">
<h1 class="fw-bold display-5">Search <span class="text-info">History</span></h1>
<p class="text-secondary lead">Browse and search through your company's AI chat archives across all connected platforms.</p>
</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>
</form>
</div>
<!-- Results Section -->
<div class="row">
<div class="col-12">
{% if query %}
<h5 class="mb-4 text-secondary">Search results for "{{ query }}"</h5>
{% else %}
<h5 class="mb-4 text-secondary">Recent Conversations</h5>
{% endif %}
{% 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 }} &bull; {{ 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>
<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>
<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 %}

View File

@ -5,38 +5,38 @@
{% block content %}
<div class="container">
<!-- 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">
<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>
</h1>
<p class="lead text-secondary mb-5">
Retrieve, archive, and search through your entire AI chat history from ChatGPT, Bing, and more.
<p class="lead text-secondary mb-5 opacity-75">
Retrieve, archive, and search through your entire AI chat history from ChatGPT, POE, Merlin, and more.
One unified search for all your past conversations.
</p>
<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="#how-it-works" class="btn btn-outline-light btn-lg px-5 py-3">How it Works</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-info btn-lg px-5 py-3">How it Works</a>
</div>
</div>
<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="d-flex align-items-center mb-4">
<div class="bg-info bg-opacity-10 p-2 rounded-3 me-3">
<i data-lucide="search" class="text-info"></i>
<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" size="32"></i>
</div>
<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-10 p-2 rounded w-50"></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-pill w-50"></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">
> Retrieving histories...<br>
> Found 142 chats from ChatGPT<br>
> Found 89 chats from Perplexity<br>
> Indexing for rapid search...
<span class="text-info">></span> Retrieving histories...<br>
<span class="text-info">></span> Found 142 chats from ChatGPT<br>
<span class="text-info">></span> Found 89 chats from POE<br>
<span class="text-info">></span> Indexing for rapid search...
</div>
</div>
</div>
@ -46,65 +46,38 @@
<!-- How It Works Section -->
<div id="how-it-works" class="py-5 mt-5">
<div class="text-center mb-5">
<h2 class="fw-bold h1">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>
<h2 class="fw-bold display-5 text-white">Three Steps to <span class="text-info">Knowledge Mastery</span></h2>
<p class="text-secondary lead opacity-75">Our platform bridges the gap between AI silos and your team's memory.</p>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="glass-card p-4 h-100 border-top border-info border-3">
<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>
<h5 class="mb-0">Connect Engines</h5>
<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>
<h4 class="mb-0 text-white">Connect Engines</h4>
</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 class="col-md-4">
<div class="glass-card p-4 h-100 border-top border-info border-3">
<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>
<h5 class="mb-0">Sync History</h5>
<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>
<h4 class="mb-0 text-white">Sync History</h4>
</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 class="col-md-4">
<div class="glass-card p-4 h-100 border-top border-info border-3">
<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>
<h5 class="mb-0">Search & Find</h5>
<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>
<h4 class="mb-0 text-white">Search & Find</h4>
</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>
</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>
<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>
</div>
{% endblock %}
{% endblock %}

View File

@ -5,4 +5,5 @@ urlpatterns = [
path('', views.index, name='index'),
path('dashboard/', views.dashboard, name='dashboard'),
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 .models import AIChatHistory, Profile
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):
if request.user.is_authenticated:
@ -10,25 +12,59 @@ def index(request):
@login_required
def dashboard(request):
profile = request.user.profile
company = profile.company
query = request.GET.get('q', '')
profile = Profile.objects.get(user=request.user)
chats = AIChatHistory.objects.filter(company=profile.company)
histories = AIChatHistory.objects.filter(company=company)
if query:
chats = chats.filter(
Q(chat_title__icontains=query) | Q(chat_content__icontains=query)
).order_by('-chat_last_date')
else:
chats = chats.order_by('-chat_last_date')[:10]
histories = histories.filter(
Q(chat_title__icontains=query) |
Q(chat_content__icontains=query)
)
# 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 = {
'chats': chats,
'histories': histories,
'query': query,
'company': profile.company
'ai_configs': ai_configs,
'last_sync_log': last_sync_log,
}
return render(request, 'core/dashboard.html', context)
@login_required
def chat_detail(request, pk):
profile = Profile.objects.get(user=request.user)
chat = AIChatHistory.objects.get(pk=pk, company=profile.company)
return render(request, 'core/chat_detail.html', {'chat': chat})
profile = request.user.profile
chat = get_object_or_404(AIChatHistory, pk=pk, company=profile.company)
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
mysqlclient==2.2.7
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;
}