Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b627f4c632 | ||
|
|
f9ad3d1087 | ||
|
|
13a2f33af9 | ||
|
|
c5b112bb09 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -180,3 +180,6 @@ if EMAIL_USE_SSL:
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = 'dashboard'
|
||||||
|
LOGOUT_REDIRECT_URL = 'index'
|
||||||
@ -1,19 +1,3 @@
|
|||||||
"""
|
|
||||||
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.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -21,6 +5,7 @@ from django.conf.urls.static import static
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/ai_service.cpython-311.pyc
Normal file
BIN
core/__pycache__/ai_service.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
BIN
core/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -1,3 +1,36 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import Company, Profile, AIChatHistory, AIConfiguration, SyncHistoryLog
|
||||||
|
from .utils import encrypt_value, decrypt_value
|
||||||
|
|
||||||
# Register your models here.
|
@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')
|
||||||
|
|||||||
176
core/ai_service.py
Normal file
176
core/ai_service.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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
|
||||||
52
core/migrations/0001_initial.py
Normal file
52
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# 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')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 16:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AIConfiguration',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('provider', models.CharField(choices=[('openai', 'OpenAI'), ('perplexity', 'Perplexity')], max_length=50)),
|
||||||
|
('api_key', models.CharField(max_length=512)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('last_sync', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ai_configs', to='core.company')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='aichathistory',
|
||||||
|
name='ai_configuration',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_histories', to='core.aiconfiguration'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SyncHistoryLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('success', 'Success'), ('error', 'Error')], max_length=50)),
|
||||||
|
('records_synced', models.IntegerField(default=0)),
|
||||||
|
('error_message', models.TextField(blank=True, null=True)),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company')),
|
||||||
|
('configuration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.aiconfiguration')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0003_alter_aiconfiguration_provider.py
Normal file
18
core/migrations/0003_alter_aiconfiguration_provider.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 18:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_aiconfiguration_aichathistory_ai_configuration_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aiconfiguration',
|
||||||
|
name='provider',
|
||||||
|
field=models.CharField(choices=[('openai', 'OpenAI'), ('perplexity', 'Perplexity'), ('merlin', 'Merlin AI'), ('poe', 'POE')], max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0004_alter_aiconfiguration_provider.py
Normal file
18
core/migrations/0004_alter_aiconfiguration_provider.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 18:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_alter_aiconfiguration_provider'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aiconfiguration',
|
||||||
|
name='provider',
|
||||||
|
field=models.CharField(choices=[('openai', 'OpenAI'), ('perplexity', 'Perplexity'), ('merlin', 'Merlin AI'), ('poe', 'POE'), ('openrouter', 'OpenRouter (Free Tiers)')], max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
core/migrations/0005_alter_aichathistory_options.py
Normal file
17
core/migrations/0005_alter_aichathistory_options.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 19:20
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_alter_aiconfiguration_provider'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='aichathistory',
|
||||||
|
options={'ordering': ['-chat_last_date'], 'verbose_name_plural': 'AI Chat Histories'},
|
||||||
|
),
|
||||||
|
]
|
||||||
17
core/migrations/0006_alter_aichathistory_unique_together.py
Normal file
17
core/migrations/0006_alter_aichathistory_unique_together.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-08 19:29
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_alter_aichathistory_options'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='aichathistory',
|
||||||
|
unique_together={('company', 'ai_chat_id')},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
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,3 +1,68 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
# Create your models here.
|
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})"
|
||||||
|
|||||||
15
core/templates/admin/base_site.html
Normal file
15
core/templates/admin/base_site.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | AI Chat Archive Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/admin_custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
<h1 id="site-name"><a href="{% url 'admin:index' %}">AI Chat Archive Admin</a></h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav-global %}{% endblock %}
|
||||||
@ -1,25 +1,153 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if project_description %}
|
<title>{% block title %}AI Chat Archive{% endblock %}</title>
|
||||||
<meta name="description" content="{{ project_description }}">
|
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<!-- Google Fonts -->
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
{% endif %}
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
{% if project_image_url %}
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Source+Code+Pro&display=swap" rel="stylesheet">
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<!-- Bootstrap 5 CDN -->
|
||||||
{% endif %}
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
{% load static %}
|
<!-- Bootstrap Icons -->
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
{% block head %}{% endblock %}
|
|
||||||
|
<!-- 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 %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||||
</body>
|
<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 %}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
60
core/templates/core/chat_detail.html
Normal file
60
core/templates/core/chat_detail.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{% 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 %}
|
||||||
151
core/templates/core/dashboard.html
Normal file
151
core/templates/core/dashboard.html
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
{% 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,145 +1,83 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}Welcome to AI Chat Archive{% 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 %}
|
{% block content %}
|
||||||
<main>
|
<div class="container">
|
||||||
<div class="card">
|
<!-- Hero Section -->
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<div class="row min-vh-75 align-items-center py-5">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<div class="col-lg-6">
|
||||||
<span class="sr-only">Loading…</span>
|
<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>
|
||||||
</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>
|
<!-- How It Works Section -->
|
||||||
<p class="runtime">
|
<div id="how-it-works" class="py-5 mt-5">
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
<div class="text-center mb-5">
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
<h2 class="fw-bold display-5 text-white">Three Steps to <span class="text-info">Knowledge Mastery</span></h2>
|
||||||
</p>
|
<p class="text-secondary lead opacity-75">Our platform bridges the gap between AI silos and your team's memory.</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<div class="row g-4">
|
||||||
<footer>
|
<div class="col-md-4">
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
<div class="glass-card p-4 h-100 border-top border-info border-3">
|
||||||
</footer>
|
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
44
core/templates/registration/login.html
Normal file
44
core/templates/registration/login.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% 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,7 +1,9 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
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
34
core/utils.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from django.conf import settings
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
|
||||||
|
def get_fernet():
|
||||||
|
# Derive a key from Django's SECRET_KEY
|
||||||
|
password = settings.SECRET_KEY.encode()
|
||||||
|
salt = b'flatlogic_salt' # In production, this should be unique and stored
|
||||||
|
kdf = PBKDF2HMAC(
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
length=32,
|
||||||
|
salt=salt,
|
||||||
|
iterations=100000,
|
||||||
|
)
|
||||||
|
key = base64.urlsafe_b64encode(kdf.derive(password))
|
||||||
|
return Fernet(key)
|
||||||
|
|
||||||
|
def encrypt_value(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
f = get_fernet()
|
||||||
|
return f.encrypt(value.encode()).decode()
|
||||||
|
|
||||||
|
def decrypt_value(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
f = get_fernet()
|
||||||
|
try:
|
||||||
|
return f.decrypt(value.encode()).decode()
|
||||||
|
except Exception:
|
||||||
|
return "ERROR_DECRYPTING"
|
||||||
@ -1,25 +1,70 @@
|
|||||||
import os
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
import platform
|
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
|
||||||
|
|
||||||
from django import get_version as django_version
|
def index(request):
|
||||||
from django.shortcuts import render
|
if request.user.is_authenticated:
|
||||||
from django.utils import timezone
|
return redirect('dashboard')
|
||||||
|
return render(request, 'core/index.html')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard(request):
|
||||||
|
profile = request.user.profile
|
||||||
|
company = profile.company
|
||||||
|
|
||||||
def home(request):
|
query = request.GET.get('q', '')
|
||||||
"""Render the landing screen with loader and environment details."""
|
histories = AIChatHistory.objects.filter(company=company)
|
||||||
host_name = request.get_host().lower()
|
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
if query:
|
||||||
now = timezone.now()
|
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 = {
|
context = {
|
||||||
"project_name": "New Style",
|
'histories': histories,
|
||||||
"agent_brand": agent_brand,
|
'query': query,
|
||||||
"django_version": django_version(),
|
'ai_configs': ai_configs,
|
||||||
"python_version": platform.python_version(),
|
'last_sync_log': last_sync_log,
|
||||||
"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/index.html", context)
|
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')
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
Django==5.2.7
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
|
cryptography==42.0.5
|
||||||
|
httpx==0.27.0
|
||||||
53
setup_sync_demo.py
Normal file
53
setup_sync_demo.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from core.models import Company, Profile, AIConfiguration
|
||||||
|
from core.utils import encrypt_value
|
||||||
|
|
||||||
|
def setup_demo():
|
||||||
|
# 1. Get or create demo company
|
||||||
|
company, _ = Company.objects.get_or_create(
|
||||||
|
tenant_id='telecom-corp',
|
||||||
|
defaults={'name': 'Telecom Corp'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Get or create admin user
|
||||||
|
user = User.objects.filter(username='admin').first()
|
||||||
|
if not user:
|
||||||
|
user = User.objects.create_superuser('admin', 'admin@example.com', 'admin123')
|
||||||
|
|
||||||
|
# 3. Ensure user has a profile with the correct company
|
||||||
|
profile, created = Profile.objects.get_or_create(
|
||||||
|
user=user,
|
||||||
|
defaults={'company': company, 'role': 'admin'}
|
||||||
|
)
|
||||||
|
if not created and profile.company != company:
|
||||||
|
print(f"Updating {user.username}'s company from {profile.company.name} to {company.name}")
|
||||||
|
profile.company = company
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
# 4. Add AI Configurations
|
||||||
|
providers = [
|
||||||
|
('openai', 'sk-proj-demo-12345'),
|
||||||
|
('perplexity', 'pplx-demo-67890'),
|
||||||
|
('merlin', 'merlin-free-tier-key'),
|
||||||
|
('poe', 'pb-poe-access-token'),
|
||||||
|
('openrouter', 'sk-or-v1-free-key')
|
||||||
|
]
|
||||||
|
|
||||||
|
for provider, key in providers:
|
||||||
|
encrypted_key = encrypt_value(key)
|
||||||
|
AIConfiguration.objects.update_or_create(
|
||||||
|
company=company,
|
||||||
|
provider=provider,
|
||||||
|
defaults={'api_key': encrypted_key, 'is_active': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Demo setup complete. Added OpenAI, Perplexity, Merlin AI, POE, and OpenRouter configurations.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup_demo()
|
||||||
217
static/css/admin_custom.css
Normal file
217
static/css/admin_custom.css
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/* Custom styles for Django Admin to match the main site CI */
|
||||||
|
:root {
|
||||||
|
--primary-bg: #0f172a;
|
||||||
|
--accent-cyan: #22d3ee;
|
||||||
|
--text-main: #f1f5f9;
|
||||||
|
--glass-bg: rgba(255, 255, 255, 0.05);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--darker-bg: #020617;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--primary-bg) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
background: rgba(15, 23, 42, 0.8) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 15px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header a:link, #header a:visited {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#branding h1 {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.breadcrumbs {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
padding: 10px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.breadcrumbs a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module {
|
||||||
|
background: var(--glass-bg) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
margin-bottom: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module h2, .module caption, .inline-group h2 {
|
||||||
|
background: rgba(255, 255, 255, 0.03) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-main {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
padding: 20px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead th {
|
||||||
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td, table th {
|
||||||
|
border-bottom: 1px solid var(--glass-border) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter {
|
||||||
|
background: rgba(15, 23, 42, 0.9) !important;
|
||||||
|
border-left: 1px solid var(--glass-border) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter h2 {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter a {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#changelist-filter li.selected a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
border-left: 3px solid var(--accent-cyan) !important;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
||||||
|
background: var(--accent-cyan) !important;
|
||||||
|
color: var(--primary-bg) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
padding: 10px 20px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover, input[type=submit]:hover {
|
||||||
|
background: #06b6d4 !important;
|
||||||
|
box-shadow: 0 0 15px rgba(34, 211, 238, 0.4) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.default, input[type=submit].default {
|
||||||
|
background: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.closelink, .button.deletelink {
|
||||||
|
background: #ef4444 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .aligned label {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text], input[type=password], input[type=email], input[type=url], input[type=number], textarea, select {
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 0.4rem !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
border-color: var(--accent-cyan) !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 0.8rem !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.02) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-tools {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-tools a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar {
|
||||||
|
background: rgba(15, 23, 42, 0.95) !important;
|
||||||
|
border-right: 1px solid var(--glass-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar .section th {
|
||||||
|
background: rgba(255, 255, 255, 0.03) !important;
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
padding: 12px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar a {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-sidebar tr.selected a {
|
||||||
|
color: var(--accent-cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific fix for the login page */
|
||||||
|
.login body {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #container {
|
||||||
|
background: var(--glass-bg) !important;
|
||||||
|
backdrop-filter: blur(15px) !important;
|
||||||
|
border: 1px solid var(--glass-border) !important;
|
||||||
|
border-radius: 1.5rem !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 450px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login #header {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user