Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -180,6 +180,3 @@ if EMAIL_USE_SSL:
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
LOGIN_REDIRECT_URL = 'dashboard'
|
||||
LOGOUT_REDIRECT_URL = 'index'
|
||||
@ -1,3 +1,19 @@
|
||||
"""
|
||||
URL configuration for config project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.conf import settings
|
||||
@ -5,7 +21,6 @@ from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,36 +1,3 @@
|
||||
from django.contrib import admin
|
||||
from .models import Company, Profile, AIChatHistory, AIConfiguration, SyncHistoryLog
|
||||
from .utils import encrypt_value, decrypt_value
|
||||
|
||||
@admin.register(Company)
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'tenant_id', 'created_at')
|
||||
search_fields = ('name', 'tenant_id')
|
||||
|
||||
@admin.register(Profile)
|
||||
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', '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')
|
||||
# Register your models here.
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
import httpx
|
||||
import logging
|
||||
import random
|
||||
import hashlib
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from datetime import timedelta
|
||||
from .models import AIConfiguration, AIChatHistory, SyncHistoryLog
|
||||
from .utils import decrypt_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def sync_all_configurations():
|
||||
configs = AIConfiguration.objects.filter(is_active=True)
|
||||
for config in configs:
|
||||
sync_ai_history(config)
|
||||
|
||||
def sync_ai_history(config: AIConfiguration):
|
||||
api_key = decrypt_value(config.api_key)
|
||||
provider = config.provider
|
||||
company = config.company
|
||||
|
||||
log = SyncHistoryLog.objects.create(
|
||||
company=company,
|
||||
configuration=config,
|
||||
status='success',
|
||||
records_synced=0
|
||||
)
|
||||
|
||||
try:
|
||||
if provider == 'openai':
|
||||
records = fetch_openai_history(api_key)
|
||||
elif provider == 'perplexity':
|
||||
records = fetch_perplexity_history(api_key)
|
||||
elif provider == 'merlin':
|
||||
records = fetch_merlin_history(api_key)
|
||||
elif provider == 'poe':
|
||||
records = fetch_poe_history(api_key)
|
||||
elif provider == 'openrouter':
|
||||
records = fetch_openrouter_history(api_key)
|
||||
else:
|
||||
records = []
|
||||
|
||||
synced_count = 0
|
||||
for record in records:
|
||||
# Simple deduplication based on ai_chat_id and company
|
||||
obj, created = AIChatHistory.objects.update_or_create(
|
||||
company=company,
|
||||
ai_chat_id=record['id'],
|
||||
defaults={
|
||||
'ai_configuration': config,
|
||||
'ai_chat_engine': provider.upper() if provider in ['poe', 'openrouter'] else provider.capitalize(),
|
||||
'chat_title': record['title'],
|
||||
'chat_content': record['content'],
|
||||
'chat_last_date': record['last_date'],
|
||||
}
|
||||
)
|
||||
if created:
|
||||
synced_count += 1
|
||||
|
||||
log.records_synced = synced_count
|
||||
log.save()
|
||||
|
||||
config.last_sync = timezone.now()
|
||||
config.save()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error syncing {provider} for {company.name}")
|
||||
log.status = 'error'
|
||||
log.error_message = str(e)
|
||||
log.save()
|
||||
|
||||
def _generate_stable_id(title, provider):
|
||||
"""Generates a stable ID based on the title and provider for mock data."""
|
||||
hash_object = hashlib.md5(f"{provider}:{title}".encode())
|
||||
return f"{provider[:2]}_{hash_object.hexdigest()[:8]}"
|
||||
|
||||
def fetch_openai_history(api_key):
|
||||
if api_key == "ERROR_DECRYPTING":
|
||||
raise Exception("API Key decryption failed")
|
||||
|
||||
data = [
|
||||
{
|
||||
'title': 'Optimizing Telecom Network Architecture',
|
||||
'content': 'Discussion about 5G deployment and latency optimization in rural areas. Focus on edge computing and MIMO technologies.',
|
||||
'last_date': timezone.now() - timedelta(hours=5)
|
||||
},
|
||||
{
|
||||
'title': 'Customer Churn Analysis Script',
|
||||
'content': 'Python script using pandas to analyze monthly billing cycles and identify high-risk accounts based on data usage patterns.',
|
||||
'last_date': timezone.now() - timedelta(days=2)
|
||||
},
|
||||
{
|
||||
'title': 'VoIP Quality Troubleshooting Guide',
|
||||
'content': 'Steps to identify packet loss and jitter in enterprise VoIP setups. Recommended buffer settings and QoS tagging.',
|
||||
'last_date': timezone.now() - timedelta(days=3)
|
||||
}
|
||||
]
|
||||
for item in data:
|
||||
item['id'] = _generate_stable_id(item['title'], 'openai')
|
||||
return data
|
||||
|
||||
def fetch_perplexity_history(api_key):
|
||||
data = [
|
||||
{
|
||||
'title': 'Latest Trends in Satellite Communication 2026',
|
||||
'content': 'Summary of Starlink and Kuiper project progress as of February 2026. Includes regulatory changes in EMEA region.',
|
||||
'last_date': timezone.now() - timedelta(minutes=15)
|
||||
},
|
||||
{
|
||||
'title': 'Regulatory Impact on 6G Spectrum',
|
||||
'content': 'Analysis of FCC and ITU recent publications regarding terahertz frequency allocations for early 6G trials.',
|
||||
'last_date': timezone.now() - timedelta(days=1)
|
||||
}
|
||||
]
|
||||
for item in data:
|
||||
item['id'] = _generate_stable_id(item['title'], 'perplexity')
|
||||
return data
|
||||
|
||||
def fetch_merlin_history(api_key):
|
||||
data = [
|
||||
{
|
||||
'title': 'Merlin AI - Project Planning SEO',
|
||||
'content': 'SEO strategy for a new telecommunications landing page targeting enterprise clients. Keywords: SD-WAN, Managed Security.',
|
||||
'last_date': timezone.now() - timedelta(hours=1)
|
||||
}
|
||||
]
|
||||
for item in data:
|
||||
item['id'] = _generate_stable_id(item['title'], 'merlin')
|
||||
return data
|
||||
|
||||
def fetch_poe_history(api_key):
|
||||
if api_key == "YOUR_MOCK_KEY_OR_EMPTY":
|
||||
# For now, it's returning mock data:
|
||||
data = [
|
||||
{
|
||||
'title': 'POE - Python Data Visualization',
|
||||
'content': 'Generating heatmaps for network traffic distribution across various regional nodes using Seaborn and Matplotlib.',
|
||||
'last_date': timezone.now() - timedelta(hours=2)
|
||||
},
|
||||
{
|
||||
'title': 'Automated Network Incident Response',
|
||||
'content': 'Conceptual workflow for using LLMs to parse syslog errors and suggest immediate remediation steps for NOC engineers.',
|
||||
'last_date': timezone.now() - timedelta(hours=8)
|
||||
}
|
||||
]
|
||||
for item in data:
|
||||
item['id'] = _generate_stable_id(item['title'], 'poe')
|
||||
return data
|
||||
|
||||
# Calling an API endpoint (Replace URL with the actual Poe API you use)
|
||||
response = httpx.get(
|
||||
"https://api.poe.com/v1",
|
||||
headers={"Authorization": f"Bearer {api_key}"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
# Transform the real API response to match our internal format:
|
||||
return [{
|
||||
'id': item['chatId'],
|
||||
'title': item['title'],
|
||||
'content': item['snippet'],
|
||||
'last_date': parse_datetime(item['updatedAt']) or timezone.now()
|
||||
} for item in data['chats']]
|
||||
|
||||
def fetch_openrouter_history(api_key):
|
||||
data = [
|
||||
{
|
||||
'title': 'OpenRouter - Llama 3 Research',
|
||||
'content': 'Comparing performance of open-source models for automated customer support in Telecom. Benchmarks on response latency.',
|
||||
'last_date': timezone.now() - timedelta(hours=3)
|
||||
}
|
||||
]
|
||||
for item in data:
|
||||
item['id'] = _generate_stable_id(item['title'], 'openrouter')
|
||||
return data
|
||||
@ -1,52 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-08 15:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Company',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('tenant_id', models.SlugField(unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('admin', 'Admin'), ('user', 'End-User')], default='user', max_length=50)),
|
||||
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='core.company')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AIChatHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ai_chat_engine', models.CharField(max_length=100)),
|
||||
('ai_chat_id', models.CharField(max_length=255)),
|
||||
('chat_title', models.CharField(max_length=512)),
|
||||
('chat_content', models.TextField()),
|
||||
('chat_last_date', models.DateTimeField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_histories', to='core.company')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'AI Chat Histories',
|
||||
'indexes': [models.Index(fields=['chat_title'], name='core_aichat_chat_ti_6da4cc_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,43 +0,0 @@
|
||||
# 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@ -1,17 +0,0 @@
|
||||
# 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'},
|
||||
),
|
||||
]
|
||||
@ -1,17 +0,0 @@
|
||||
# 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.
Binary file not shown.
Binary file not shown.
@ -1,68 +1,3 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class Company(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
tenant_id = models.SlugField(unique=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='members')
|
||||
role = models.CharField(max_length=50, choices=[('admin', 'Admin'), ('user', 'End-User')], default='user')
|
||||
|
||||
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)
|
||||
chat_content = models.TextField()
|
||||
chat_last_date = models.DateTimeField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "AI Chat Histories"
|
||||
ordering = ['-chat_last_date']
|
||||
unique_together = ('company', 'ai_chat_id')
|
||||
indexes = [
|
||||
models.Index(fields=['chat_title']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
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})"
|
||||
# Create your models here.
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1,153 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}AI Chat Archive{% endblock %}</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Source+Code+Pro&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
|
||||
<style>
|
||||
: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);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 1rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: rgba(15, 23, 42, 0.8) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.btn-cyan {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--primary-bg);
|
||||
font-weight: 600;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-cyan:hover {
|
||||
background-color: #06b6d4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgba(34, 211, 238, 0.2);
|
||||
color: var(--accent-cyan);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.x-small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{% url 'index' %}">
|
||||
<i data-lucide="archive" class="me-2 text-info"></i>
|
||||
<span class="fw-bold">AI Chat Archive</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">Admin</a>
|
||||
</li>
|
||||
<li class="nav-item ms-lg-3">
|
||||
<form action="{% url 'logout' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-light btn-sm">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-cyan px-4" href="{% url 'login' %}">Login</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="py-5 flex-grow-1">
|
||||
<div class="container">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<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 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="py-4 border-top border-secondary mt-auto">
|
||||
<div class="container text-center text-secondary small">
|
||||
© 2026 AI Chat Archive. Powered by Flatlogic.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,60 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ chat.chat_title }} - AI Chat Archive{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<nav aria-label="breadcrumb" class="mb-5">
|
||||
<ol class="breadcrumb">
|
||||
<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-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 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-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 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 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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
const text = document.getElementById('chat-body').innerText;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('Chat content copied to clipboard!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,151 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Dashboard - AI History Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
<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 %}
|
||||
@ -1,83 +1,145 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Welcome to AI Chat Archive{% endblock %}
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<!-- Hero Section -->
|
||||
<div class="row min-vh-75 align-items-center py-5">
|
||||
<div class="col-lg-6">
|
||||
<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 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 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 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-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-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 border-opacity-25 pt-4">
|
||||
<div class="chat-content text-secondary opacity-50">
|
||||
<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>
|
||||
</div>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<div id="how-it-works" class="py-5 mt-5">
|
||||
<div class="text-center mb-5">
|
||||
<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-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">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-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>"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-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 powerful search bar to instantly find any past prompt or answer across all connected AI tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
@ -1,44 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - AI Chat Archive{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container d-flex align-items-center justify-content-center min-vh-75">
|
||||
<div class="glass-card p-5 w-100" style="max-width: 450px;">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="fw-bold">Welcome Back</h2>
|
||||
<p class="text-secondary">Sign in to access your AI vault</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger bg-danger bg-opacity-10 border-danger text-danger mb-4">
|
||||
Your username and password didn't match. Please try again.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label text-secondary small text-uppercase fw-bold">Username</label>
|
||||
<input type="text" name="username" autofocus maxlength="150" required id="id_username"
|
||||
class="form-control bg-dark bg-opacity-50 border-secondary text-white py-3">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="id_password" class="form-label text-secondary small text-uppercase fw-bold">Password</label>
|
||||
<input type="password" name="password" required id="id_password"
|
||||
class="form-control bg-dark bg-opacity-50 border-secondary text-white py-3">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-cyan w-100 py-3 fw-bold">Sign In</button>
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4 pt-4 border-top border-secondary border-opacity-25">
|
||||
<p class="text-secondary small">Demo credentials: <strong>admin</strong> / <strong>admin123</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,9 +1,7 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
from .views import home
|
||||
|
||||
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'),
|
||||
path("", home, name="home"),
|
||||
]
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
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,70 +1,25 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Q
|
||||
from django.contrib import messages
|
||||
from .models import AIChatHistory, AIConfiguration, SyncHistoryLog
|
||||
from .ai_service import sync_ai_history
|
||||
import os
|
||||
import platform
|
||||
|
||||
def index(request):
|
||||
if request.user.is_authenticated:
|
||||
return redirect('dashboard')
|
||||
return render(request, 'core/index.html')
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
profile = request.user.profile
|
||||
company = profile.company
|
||||
|
||||
query = request.GET.get('q', '')
|
||||
histories = AIChatHistory.objects.filter(company=company)
|
||||
|
||||
if query:
|
||||
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()
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
|
||||
context = {
|
||||
'histories': histories,
|
||||
'query': query,
|
||||
'ai_configs': ai_configs,
|
||||
'last_sync_log': last_sync_log,
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, 'core/dashboard.html', context)
|
||||
|
||||
@login_required
|
||||
def chat_detail(request, pk):
|
||||
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')
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
cryptography==42.0.5
|
||||
httpx==0.27.0
|
||||
@ -1,53 +0,0 @@
|
||||
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()
|
||||
@ -1,217 +0,0 @@
|
||||
/* 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