Compare commits
10 Commits
330b813563
...
705b35bbc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 705b35bbc3 | |||
|
|
63cf731d1f | ||
|
|
4c1df17a46 | ||
|
|
1566d6c207 | ||
|
|
e2769f2d2d | ||
|
|
3b7559b157 | ||
|
|
b700e16dec | ||
|
|
94ac62d585 | ||
|
|
e4f2931b77 | ||
|
|
864ef909c8 |
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
@ -382,17 +382,11 @@ def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[st
|
|||||||
"data": decoded if decoded is not None else response_body,
|
"data": decoded if decoded is not None else response_body,
|
||||||
}
|
}
|
||||||
|
|
||||||
error_message = "AI proxy request failed"
|
|
||||||
if isinstance(decoded, dict):
|
|
||||||
error_message = decoded.get("error") or decoded.get("message") or error_message
|
|
||||||
elif response_body:
|
|
||||||
error_message = response_body
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"status": status,
|
"status": status,
|
||||||
"error": error_message,
|
"error": "AI proxy request failed",
|
||||||
"response": decoded if decoded is not None else response_body,
|
"data": decoded if decoded is not None else response_body,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
BIN
assets/pasted-20251119-223636-11a12b2c.png
Normal file
BIN
assets/pasted-20251119-223636-11a12b2c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
assets/pasted-20251119-224750-5fe94e0e.png
Normal file
BIN
assets/pasted-20251119-224750-5fe94e0e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
assets/pasted-20251119-225244-bb3fcbab.png
Normal file
BIN
assets/pasted-20251119-225244-bb3fcbab.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
assets/pasted-20251120-002324-c898244a.png
Normal file
BIN
assets/pasted-20251120-002324-c898244a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
assets/vm-shot-2025-11-19T22-56-10-873Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-19T22-56-10-873Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
@ -152,3 +152,17 @@ STATICFILES_DIRS = [
|
|||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Article, TodoItem
|
from .models import Article, TodoItem, Setting, Conversation, Message
|
||||||
|
|
||||||
admin.site.register(Article)
|
admin.site.register(Article)
|
||||||
admin.site.register(TodoItem)
|
admin.site.register(TodoItem)
|
||||||
|
admin.site.register(Setting)
|
||||||
|
admin.site.register(Conversation)
|
||||||
|
admin.site.register(Message)
|
||||||
|
|||||||
32
core/migrations/0004_conversation_message.py
Normal file
32
core/migrations/0004_conversation_message.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-19 22:34
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_todoitem_description_todoitem_tags'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Conversation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=200)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Message',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('is_from_user', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='core.conversation')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-19 23:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_conversation_message'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='message',
|
||||||
|
options={'ordering': ['created_at']},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='message',
|
||||||
|
name='is_from_user',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='message',
|
||||||
|
name='sender',
|
||||||
|
field=models.CharField(choices=[('user', 'User'), ('ai', 'AI'), ('system', 'System'), ('ai_command', 'AI Command')], default='user', max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
21
core/migrations/0006_setting.py
Normal file
21
core/migrations/0006_setting.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-20 10:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_alter_message_options_remove_message_is_from_user_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Setting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('key', models.CharField(max_length=255, unique=True)),
|
||||||
|
('value', models.TextField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0007_conversation_is_generating.py
Normal file
18
core/migrations/0007_conversation_is_generating.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-20 19:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_setting'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conversation',
|
||||||
|
name='is_generating',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0006_setting.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0006_setting.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -24,3 +24,30 @@ class TodoItem(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
class Conversation(models.Model):
|
||||||
|
title = models.CharField(max_length=200)
|
||||||
|
is_generating = models.BooleanField(default=False)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Message(models.Model):
|
||||||
|
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages')
|
||||||
|
content = models.TextField()
|
||||||
|
sender = models.CharField(max_length=20, choices=[('user', 'User'), ('ai', 'AI'), ('system', 'System'), ('ai_command', 'AI Command')], default='user')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Message from {self.get_sender_display()} at {self.created_at}"
|
||||||
|
|
||||||
|
class Setting(models.Model):
|
||||||
|
key = models.CharField(max_length=255, unique=True)
|
||||||
|
value = models.TextField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.key
|
||||||
|
|||||||
@ -15,17 +15,23 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{% url 'index' %}">AI Task Manager</a>
|
<a class="navbar-brand" href="{% url 'core:index' %}">AI Task Manager</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'index' %}">Table View</a>
|
<a class="nav-link" href="{% url 'core:index' %}">Table View</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'kanban_board' %}">Kanban Board</a>
|
<a class="nav-link" href="{% url 'core:kanban' %}">Kanban Board</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'core:chat' %}">Chat</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'core:settings' %}">Settings</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
19
core/templates/base_chat.html
Normal file
19
core/templates/base_chat.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Gemini Chat{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<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&family=Poppins:wght@600&display=swap" rel="stylesheet">
|
||||||
|
{% load static %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ timestamp }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
284
core/templates/core/chat.html
Normal file
284
core/templates/core/chat.html
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="chat-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="chat-sidebar">
|
||||||
|
<form method="post" action="{% url 'core:chat' %}" class="new-chat-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" name="title" placeholder="New conversation title" required>
|
||||||
|
<button type="submit" class="new-chat-btn">+ New Chat</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ul class="conversation-list">
|
||||||
|
{% for conv in conversation_list %}
|
||||||
|
<a href="{% url 'core:chat_detail' conv.id %}" class="{% if selected_conversation.id == conv.id %}active{% endif %}">
|
||||||
|
{{ conv.title }}
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<li>No conversations yet.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Chat Area -->
|
||||||
|
<main class="chat-main">
|
||||||
|
{% if selected_conversation %}
|
||||||
|
<header class="chat-header">
|
||||||
|
<h3>{{ selected_conversation.title }}</h3>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="chat-messages" id="chat-messages">
|
||||||
|
{% for message in selected_conversation.messages.all %}
|
||||||
|
<div class="message {{ message.sender }}">
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-author">
|
||||||
|
{% if message.sender == 'user' %}
|
||||||
|
You
|
||||||
|
{% elif message.sender == 'ai' %}
|
||||||
|
AI
|
||||||
|
{% elif message.sender == 'system' %}
|
||||||
|
System
|
||||||
|
{% elif message.sender == 'ai_command' %}
|
||||||
|
AI Command
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if message.sender == 'ai_command' %}
|
||||||
|
<pre><code>{{ message.content|linebreaksbr }}</code></pre>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ message.content|linebreaksbr }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-form-container">
|
||||||
|
<form method="post" action="{% url 'core:chat_detail' selected_conversation.id %}" class="chat-form" id="chat-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<textarea name="text" placeholder="Send a message..." rows="1" id="chat-textarea"></textarea>
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="no-conversation-selected">
|
||||||
|
<div>
|
||||||
|
<h2>Gemini Chat</h2>
|
||||||
|
<p>Select a conversation or start a new one.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="loader-overlay" id="loader-overlay" style="display: none;">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const messagesContainer = document.getElementById('chat-messages');
|
||||||
|
const chatForm = document.getElementById('chat-form');
|
||||||
|
const chatTextarea = document.getElementById('chat-textarea');
|
||||||
|
const submitButton = chatForm ? chatForm.querySelector('button[type="submit"]') : null;
|
||||||
|
|
||||||
|
let pollingInterval;
|
||||||
|
|
||||||
|
// Function to scroll to the bottom of the messages
|
||||||
|
function scrollToBottom() {
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
if (chatTextarea) {
|
||||||
|
chatTextarea.addEventListener('input', () => {
|
||||||
|
chatTextarea.style.height = 'auto';
|
||||||
|
chatTextarea.style.height = (chatTextarea.scrollHeight) + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
if (chatForm && submitButton) {
|
||||||
|
chatForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(chatForm);
|
||||||
|
const messageText = formData.get('text').trim();
|
||||||
|
|
||||||
|
if (!messageText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
|
||||||
|
// Manually append the user's message to the UI immediately
|
||||||
|
appendMessage('user', messageText);
|
||||||
|
chatTextarea.value = '';
|
||||||
|
chatTextarea.style.height = 'auto';
|
||||||
|
|
||||||
|
fetch(chatForm.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': formData.get('csrfmiddlewaretoken')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
startPolling(data.conversation_id);
|
||||||
|
} else {
|
||||||
|
console.error('Error submitting message:', data.error);
|
||||||
|
// Re-enable button on error
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Send';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fetch error:', error);
|
||||||
|
// Re-enable button on error
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Send';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter to submit, Shift+Enter for new line
|
||||||
|
if (chatTextarea && chatForm) {
|
||||||
|
chatTextarea.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
chatForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendMessage(sender, content) {
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.classList.add('message', sender);
|
||||||
|
|
||||||
|
const messageContentEl = document.createElement('div');
|
||||||
|
messageContentEl.classList.add('message-content');
|
||||||
|
|
||||||
|
const authorEl = document.createElement('div');
|
||||||
|
authorEl.classList.add('message-author');
|
||||||
|
if (sender === 'user') {
|
||||||
|
authorEl.innerText = 'You';
|
||||||
|
} else if (sender === 'ai') {
|
||||||
|
authorEl.innerText = 'AI';
|
||||||
|
} else {
|
||||||
|
authorEl.innerText = sender.charAt(0).toUpperCase() + sender.slice(1);
|
||||||
|
}
|
||||||
|
messageContentEl.appendChild(authorEl);
|
||||||
|
|
||||||
|
const pEl = document.createElement('p');
|
||||||
|
pEl.innerHTML = content.replace(/\n/g, '<br>');
|
||||||
|
messageContentEl.appendChild(pEl);
|
||||||
|
|
||||||
|
messageEl.appendChild(messageContentEl);
|
||||||
|
messagesContainer.appendChild(messageEl);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMessageList(messages) {
|
||||||
|
messagesContainer.innerHTML = ''; // Clear existing messages
|
||||||
|
messages.forEach(message => {
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.classList.add('message', message.sender);
|
||||||
|
|
||||||
|
const messageContentEl = document.createElement('div');
|
||||||
|
messageContentEl.classList.add('message-content');
|
||||||
|
|
||||||
|
const authorEl = document.createElement('div');
|
||||||
|
authorEl.classList.add('message-author');
|
||||||
|
if (message.sender === 'user') {
|
||||||
|
authorEl.innerText = 'You';
|
||||||
|
} else if (message.sender === 'ai') {
|
||||||
|
authorEl.innerText = 'AI';
|
||||||
|
} else if (message.sender === 'system') {
|
||||||
|
authorEl.innerText = 'System';
|
||||||
|
} else if (message.sender === 'ai_command') {
|
||||||
|
authorEl.innerText = 'AI Command';
|
||||||
|
}
|
||||||
|
messageContentEl.appendChild(authorEl);
|
||||||
|
|
||||||
|
if (message.sender === 'ai_command') {
|
||||||
|
const preEl = document.createElement('pre');
|
||||||
|
const codeEl = document.createElement('code');
|
||||||
|
codeEl.innerHTML = message.content.replace(/\n/g, '<br>');
|
||||||
|
preEl.appendChild(codeEl);
|
||||||
|
messageContentEl.appendChild(preEl);
|
||||||
|
} else {
|
||||||
|
const pEl = document.createElement('p');
|
||||||
|
pEl.innerHTML = message.content.replace(/\n/g, '<br>');
|
||||||
|
messageContentEl.appendChild(pEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageEl.appendChild(messageContentEl);
|
||||||
|
messagesContainer.appendChild(messageEl);
|
||||||
|
});
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling(conversationId) {
|
||||||
|
if (!conversationId) return;
|
||||||
|
|
||||||
|
// Clear any existing polling interval
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollingInterval = setInterval(() => {
|
||||||
|
fetch(`/get_conversation_messages/${conversationId}/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateMessageList(data.messages);
|
||||||
|
|
||||||
|
if (!data.is_generating) {
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Send';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Polling error:', error);
|
||||||
|
clearInterval(pollingInterval); // Stop polling on error
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Send';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state check on page load
|
||||||
|
const isGenerating = {{ selected_conversation.is_generating|yesno:"true,false" }};
|
||||||
|
const conversationId = {{ selected_conversation.id|default:"null" }};
|
||||||
|
|
||||||
|
if (isGenerating && conversationId) {
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = 'Sending...';
|
||||||
|
}
|
||||||
|
startPolling(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial scroll to bottom
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
78
core/templates/core/conversation_list.html
Normal file
78
core/templates/core/conversation_list.html
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
{% extends "base_chat.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="chat-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="chat-sidebar">
|
||||||
|
<a href="{% url 'conversation_list' %}" class="new-chat-btn">+ New Chat</a>
|
||||||
|
|
||||||
|
<ul class="conversation-list">
|
||||||
|
{% for conv in conversation_list %}
|
||||||
|
<a href="{% url 'conversation_detail' conv.id %}" class="{% if selected_conversation.id == conv.id %}active{% endif %}">
|
||||||
|
{{ conv.title }}
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<li>No conversations yet.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Chat Area -->
|
||||||
|
<main class="chat-main">
|
||||||
|
{% if selected_conversation %}
|
||||||
|
<header class="chat-header">
|
||||||
|
<h3>{{ selected_conversation.title }}</h3>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="chat-messages" id="chat-messages">
|
||||||
|
{% for message in selected_conversation.messages.all %}
|
||||||
|
<div class="message {{ message.sender_type }}">
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-author">{% if message.sender_type == 'user' %}You{% else %}AI{% endif %}</div>
|
||||||
|
<p>{{ message.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-form-container">
|
||||||
|
<form method="post" action="{% url 'conversation_detail' selected_conversation.id %}" class="chat-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<textarea name="text" placeholder="Send a message..." rows="1"></textarea>
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="no-conversation-selected">
|
||||||
|
<div>
|
||||||
|
<h2>Gemini Chat</h2>
|
||||||
|
<p>Select a conversation or start a new one.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Auto-scroll to the latest message
|
||||||
|
const messagesContainer = document.getElementById('chat-messages');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
const textarea = document.querySelector('.chat-form textarea');
|
||||||
|
if (textarea) {
|
||||||
|
textarea.addEventListener('input', () => {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = (textarea.scrollHeight) + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@ -38,8 +38,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-white">
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||||
<h2 class="h5 mb-0">Your To-Do List</h2>
|
<h2 class="h5 mb-0">Your To-Do List</h2>
|
||||||
|
<form method="post" action="{% url 'core:cleanup_tasks' %}" style="display: inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-warning btn-sm">Cleanup All Tasks</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
@ -50,6 +54,7 @@
|
|||||||
<th scope="col">Tags</th>
|
<th scope="col">Tags</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">Status</th>
|
||||||
<th scope="col">Created</th>
|
<th scope="col">Created</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -66,6 +71,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td><span class="badge status-{{ item.status }}">{{ item.get_status_display }}</span></td>
|
<td><span class="badge status-{{ item.status }}">{{ item.get_status_display }}</span></td>
|
||||||
<td>{{ item.created_at|date:"M d, Y" }}</td>
|
<td>{{ item.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{% url 'core:delete_task' item.id %}" style="display: inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -25,6 +25,10 @@
|
|||||||
{% for item in tasks %}
|
{% for item in tasks %}
|
||||||
<div class="card kanban-card mb-3 shadow-sm" data-task-id="{{ item.id }}">
|
<div class="card kanban-card mb-3 shadow-sm" data-task-id="{{ item.id }}">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<form action="{% url 'core:delete_task' item.id %}" method="post" class="delete-task-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn-close" aria-label="Close"></button>
|
||||||
|
</form>
|
||||||
<h5 class="card-title h6">{{ item.title }}</h5>
|
<h5 class="card-title h6">{{ item.title }}</h5>
|
||||||
<p class="card-text small">{{ item.description|default:""|truncatewords:15 }}</p>
|
<p class="card-text small">{{ item.description|default:""|truncatewords:15 }}</p>
|
||||||
{% if item.tags %}
|
{% if item.tags %}
|
||||||
|
|||||||
18
core/templates/core/settings.html
Normal file
18
core/templates/core/settings.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="custom_instructions" class="form-label">Custom AI Instructions</label>
|
||||||
|
<textarea class="form-control" id="custom_instructions" name="custom_instructions" rows="10">{{ custom_instructions.value }}</textarea>
|
||||||
|
<div class="form-text">
|
||||||
|
These instructions will be added to the AI's system prompt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
17
core/urls.py
17
core/urls.py
@ -1,10 +1,17 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
from .views import index, article_detail, kanban_board, update_task_status
|
app_name = 'core'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index, name="index"),
|
path("", views.index, name="index"),
|
||||||
path("kanban/", kanban_board, name="kanban_board"),
|
path('kanban/', views.kanban_board, name='kanban'),
|
||||||
path("article/<int:article_id>/", article_detail, name="article_detail"),
|
path('article/<int:article_id>/', views.article_detail, name='article_detail'),
|
||||||
path('update_task_status/', update_task_status, name='update_task_status'),
|
path('update_task_status/', views.update_task_status, name='update_task_status'),
|
||||||
|
path('delete_task/<int:task_id>/', views.delete_task, name='delete_task'),
|
||||||
|
path('chat/', views.chat_view, name='chat'),
|
||||||
|
path('chat/<int:conversation_id>/', views.chat_view, name='chat_detail'),
|
||||||
|
path('cleanup_tasks/', views.cleanup_tasks, name='cleanup_tasks'),
|
||||||
|
path('settings/', views.settings_view, name='settings'),
|
||||||
|
path('get_conversation_messages/<int:conversation_id>/', views.get_conversation_messages, name='get_conversation_messages'),
|
||||||
]
|
]
|
||||||
350
core/views.py
350
core/views.py
@ -2,16 +2,23 @@ from django.shortcuts import render, redirect, get_object_or_404
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
import json
|
import json
|
||||||
from .models import Article, TodoItem
|
import logging
|
||||||
|
import threading
|
||||||
|
from .models import Article, TodoItem, Conversation, Message, Setting
|
||||||
from .forms import TodoItemForm
|
from .forms import TodoItemForm
|
||||||
import time
|
import time
|
||||||
|
from ai.local_ai_api import LocalAIApi
|
||||||
|
|
||||||
|
# Get an instance of a logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = TodoItemForm(request.POST)
|
form = TodoItemForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
return redirect('index')
|
return redirect('core:index')
|
||||||
else:
|
else:
|
||||||
form = TodoItemForm()
|
form = TodoItemForm()
|
||||||
|
|
||||||
@ -26,6 +33,7 @@ def index(request):
|
|||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
|
||||||
def kanban_board(request):
|
def kanban_board(request):
|
||||||
tasks = TodoItem.objects.all().order_by('created_at')
|
tasks = TodoItem.objects.all().order_by('created_at')
|
||||||
tasks_by_status = {
|
tasks_by_status = {
|
||||||
@ -40,10 +48,12 @@ def kanban_board(request):
|
|||||||
}
|
}
|
||||||
return render(request, "core/kanban.html", context)
|
return render(request, "core/kanban.html", context)
|
||||||
|
|
||||||
|
|
||||||
def article_detail(request, article_id):
|
def article_detail(request, article_id):
|
||||||
article = Article.objects.get(pk=article_id)
|
article = Article.objects.get(pk=article_id)
|
||||||
return render(request, "core/article_detail.html", {"article": article})
|
return render(request, "core/article_detail.html", {"article": article})
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def update_task_status(request):
|
def update_task_status(request):
|
||||||
try:
|
try:
|
||||||
@ -58,3 +68,339 @@ def update_task_status(request):
|
|||||||
return JsonResponse({'success': True})
|
return JsonResponse({'success': True})
|
||||||
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
||||||
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def delete_task(request, task_id):
|
||||||
|
task = get_object_or_404(TodoItem, id=task_id)
|
||||||
|
task.delete()
|
||||||
|
|
||||||
|
# Redirect to the previous page or a default URL
|
||||||
|
referer = request.META.get('HTTP_REFERER')
|
||||||
|
if referer:
|
||||||
|
return redirect(referer)
|
||||||
|
return redirect('core:index')
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def cleanup_tasks(request):
|
||||||
|
TodoItem.objects.all().delete()
|
||||||
|
return redirect('core:index')
|
||||||
|
|
||||||
|
|
||||||
|
def execute_command(command_data):
|
||||||
|
command_name = command_data.get('name')
|
||||||
|
args = command_data.get('args', {})
|
||||||
|
logger.info(f"Executing command: {command_name} with args: {args}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if command_name == 'send_message':
|
||||||
|
message = args.get('message')
|
||||||
|
if not message:
|
||||||
|
logger.error("Command 'send_message' failed: 'message' is a required argument.")
|
||||||
|
return "[SYSTEM] Error: 'message' is a required argument for send_message."
|
||||||
|
return message
|
||||||
|
|
||||||
|
elif command_name == 'add_task':
|
||||||
|
title = args.get('title')
|
||||||
|
if not title:
|
||||||
|
logger.error("Command 'add_task' failed: 'title' is a required argument.")
|
||||||
|
return "[SYSTEM] Error: 'title' is a required argument for add_task."
|
||||||
|
|
||||||
|
new_task = TodoItem.objects.create(
|
||||||
|
title=title,
|
||||||
|
description=args.get('description', ''),
|
||||||
|
status=args.get('status', 'todo')
|
||||||
|
)
|
||||||
|
logger.info(f"Command 'add_task' executed successfully. New task ID: {new_task.id}")
|
||||||
|
return f"[SYSTEM] Command 'add_task' executed successfully. New task ID: {new_task.id}"
|
||||||
|
|
||||||
|
elif command_name == 'edit_task':
|
||||||
|
task_id = args.get('task_id')
|
||||||
|
if not task_id:
|
||||||
|
logger.error("Command 'edit_task' failed: 'task_id' is a required argument.")
|
||||||
|
return "[SYSTEM] Error: 'task_id' is a required argument for edit_task."
|
||||||
|
|
||||||
|
task = get_object_or_404(TodoItem, id=task_id)
|
||||||
|
|
||||||
|
if 'title' in args:
|
||||||
|
task.title = args['title']
|
||||||
|
if 'description' in args:
|
||||||
|
task.description = args['description']
|
||||||
|
if 'status' in args:
|
||||||
|
task.status = args['status']
|
||||||
|
|
||||||
|
task.save()
|
||||||
|
logger.info(f"Command 'edit_task' for task ID {task_id} executed successfully.")
|
||||||
|
return f"[SYSTEM] Command 'edit_task' for task ID {task_id} executed successfully."
|
||||||
|
|
||||||
|
elif command_name == 'delete_task':
|
||||||
|
task_id = args.get('task_id')
|
||||||
|
if not task_id:
|
||||||
|
logger.error("Command 'delete_task' failed: 'task_id' is a required argument.")
|
||||||
|
return "[SYSTEM] Error: 'task_id' is a required argument for delete_task."
|
||||||
|
|
||||||
|
task = get_object_or_404(TodoItem, id=task_id)
|
||||||
|
task.delete()
|
||||||
|
logger.info(f"Command 'delete_task' for task ID {task_id} executed successfully.")
|
||||||
|
return f"[SYSTEM] Command 'delete_task' for task ID {task_id} executed successfully."
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown command received: '{command_name}'")
|
||||||
|
return f"[SYSTEM] Error: Unknown command '{command_name}'."
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing command '{command_name}': {e}", exc_info=True)
|
||||||
|
return f"[SYSTEM] Error executing command '{command_name}': {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def run_ai_process_in_background(conversation_id):
|
||||||
|
"""This function runs in a separate thread."""
|
||||||
|
try:
|
||||||
|
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||||
|
conversation.is_generating = True
|
||||||
|
conversation.save()
|
||||||
|
|
||||||
|
history = []
|
||||||
|
for msg in conversation.messages.order_by('created_at'):
|
||||||
|
role = msg.sender
|
||||||
|
if role == 'ai':
|
||||||
|
role = 'assistant'
|
||||||
|
# User messages are already 'user', system messages are 'user' for the model
|
||||||
|
elif role == 'system':
|
||||||
|
role = 'user'
|
||||||
|
history.append({"role": role, "content": msg.content})
|
||||||
|
|
||||||
|
custom_instructions, _ = Setting.objects.get_or_create(
|
||||||
|
key='custom_instructions',
|
||||||
|
defaults={'value': ''}
|
||||||
|
)
|
||||||
|
custom_instructions_text = custom_instructions.value + '\n\n' if custom_instructions.value else ''
|
||||||
|
|
||||||
|
system_message = {
|
||||||
|
"role": "system",
|
||||||
|
"content": custom_instructions_text + '''You are a project management assistant. To communicate with the user, you MUST use the `send_message` command.
|
||||||
|
|
||||||
|
**Commands must be in a specific JSON format.** Your response must be a JSON object with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": {
|
||||||
|
"name": "command_name",
|
||||||
|
"args": {
|
||||||
|
"arg1": "value1",
|
||||||
|
"arg2": "value2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Commands:**
|
||||||
|
|
||||||
|
* `send_message`: Sends a message to the user. **USE THIS FOR ALL CONVERSATIONAL RESPONSES.**
|
||||||
|
* `args`:
|
||||||
|
* `message` (string, required): The message to send to the user.
|
||||||
|
* `add_task`: Adds a new task.
|
||||||
|
* `args`:
|
||||||
|
* `title` (string, required): The title of the task.
|
||||||
|
* `description` (string, optional): The description of the task.
|
||||||
|
* `status` (string, optional, default: 'todo'): The status of the task. Can be 'todo', 'inprogress', 'done', 'blocked'.
|
||||||
|
* `edit_task`: Edits an existing task.
|
||||||
|
* `args`:
|
||||||
|
* `task_id` (integer, required): The ID of the task to edit.
|
||||||
|
* `title` (string, optional): The new title.
|
||||||
|
* `description` (string, optional): The new description.
|
||||||
|
* `status` (string, optional): The new status.
|
||||||
|
* `delete_task`: Deletes a task.
|
||||||
|
* `args`:
|
||||||
|
* `task_id` (integer, required): The ID of the task to delete.
|
||||||
|
|
||||||
|
**Execution Loop:**
|
||||||
|
|
||||||
|
1. You can issue a series of commands to be executed sequentially.
|
||||||
|
2. The system executes each command and provides a result.
|
||||||
|
3. The loop will stop if you call `send_message` or after a maximum of 7 iterations.
|
||||||
|
|
||||||
|
**VERY IMPORTANT:**
|
||||||
|
- To talk to the user, you MUST use the `send_message` command. This command will STOP the execution loop.
|
||||||
|
- If you need to perform multiple actions (e.g., add a task and then comment on it), issue the action commands (`add_task`, `edit_task`, etc.) *before* using `send_message`.
|
||||||
|
- ONLY use commands other than `send_message` if the user explicitly asks you to `add`, `edit`, or `delete` tasks.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
* **User:** "Hi, how are you?"
|
||||||
|
* **Correct AI Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": {
|
||||||
|
"name": "send_message",
|
||||||
|
"args": {
|
||||||
|
"message": "I'm doing great, thanks for asking! How can I help you with your tasks today?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **User:** "add a new task to buy milk"
|
||||||
|
* **Correct AI Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": {
|
||||||
|
"name": "add_task",
|
||||||
|
"args": {
|
||||||
|
"title": "buy milk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT:** Do not wrap the JSON command in markdown backticks or any other text. The entire response must be the JSON object.'''
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = TodoItem.objects.all().order_by('created_at')
|
||||||
|
task_list_str = "\n".join([
|
||||||
|
f"- ID {task.id}: {task.title} (Status: {task.get_status_display()}, Tags: {task.tags or 'None'})" for task in tasks
|
||||||
|
])
|
||||||
|
|
||||||
|
tasks_context = {
|
||||||
|
"role": "system",
|
||||||
|
"content": f"Here is the current list of tasks:\n{task_list_str}"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Starting AI processing loop...")
|
||||||
|
|
||||||
|
for i in range(31): # Loop up to 7 times
|
||||||
|
logger.info(f"AI loop iteration {i+1}")
|
||||||
|
|
||||||
|
response = LocalAIApi.create_response({
|
||||||
|
"input": [
|
||||||
|
system_message,
|
||||||
|
tasks_context,
|
||||||
|
] + history,
|
||||||
|
"text": {"format": {"type": "json_object"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
if not response.get("success"):
|
||||||
|
logger.error(f"AI API request failed. Full error: {response.get('error')}")
|
||||||
|
ai_text = "I couldn't process that. Please try again."
|
||||||
|
Message.objects.create(conversation=conversation, content=ai_text, sender='ai')
|
||||||
|
break
|
||||||
|
|
||||||
|
ai_text = LocalAIApi.extract_text(response)
|
||||||
|
if not ai_text:
|
||||||
|
logger.warning("AI response was empty.")
|
||||||
|
ai_text = "I couldn't process that. Please try again."
|
||||||
|
Message.objects.create(conversation=conversation, content=ai_text, sender='ai')
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
command_json = json.loads(ai_text)
|
||||||
|
if 'command' in command_json:
|
||||||
|
command_name = command_json.get('command', {}).get('name')
|
||||||
|
command_result = execute_command(command_json['command'])
|
||||||
|
|
||||||
|
sender = 'ai' if command_name == 'send_message' else 'system'
|
||||||
|
Message.objects.create(conversation=conversation, content=command_result, sender=sender)
|
||||||
|
|
||||||
|
if command_name == 'send_message':
|
||||||
|
break
|
||||||
|
|
||||||
|
history.append({"role": "user", "content": command_result})
|
||||||
|
else:
|
||||||
|
Message.objects.create(conversation=conversation, content=ai_text, sender='ai')
|
||||||
|
break
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
Message.objects.create(conversation=conversation, content=ai_text, sender='ai')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.warning("AI loop finished after 7 iterations without sending a message.")
|
||||||
|
final_message = "I seem to be stuck in a loop. Could you clarify what you'd like me to do?"
|
||||||
|
Message.objects.create(conversation=conversation, content=final_message, sender='ai')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred in background AI process: {e}", exc_info=True)
|
||||||
|
try:
|
||||||
|
# Try to inform the user about the error
|
||||||
|
Message.objects.create(conversation_id=conversation_id, content=f"An internal error occurred: {str(e)}", sender='ai')
|
||||||
|
except Exception as e2:
|
||||||
|
logger.error(f"Could not even save the error message to the conversation: {e2}", exc_info=True)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Ensure is_generating is always set to False
|
||||||
|
try:
|
||||||
|
conversation = Conversation.objects.get(id=conversation_id)
|
||||||
|
conversation.is_generating = False
|
||||||
|
conversation.save()
|
||||||
|
except Conversation.DoesNotExist:
|
||||||
|
logger.error(f"Conversation with ID {conversation_id} does not exist when trying to finalize background process.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not finalize background process for conversation {conversation_id}: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def chat_view(request, conversation_id=None):
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Create a new conversation
|
||||||
|
if 'title' in request.POST:
|
||||||
|
title = request.POST.get('title', 'New Conversation').strip()
|
||||||
|
if not title:
|
||||||
|
title = 'New Conversation'
|
||||||
|
conversation = Conversation.objects.create(title=title)
|
||||||
|
return redirect('core:chat_detail', conversation_id=conversation.id)
|
||||||
|
|
||||||
|
# Send a message in an existing conversation
|
||||||
|
elif 'text' in request.POST and conversation_id:
|
||||||
|
text = request.POST.get('text').strip()
|
||||||
|
if text:
|
||||||
|
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||||
|
Message.objects.create(conversation=conversation, content=text, sender='user')
|
||||||
|
|
||||||
|
# Set is_generating to True
|
||||||
|
conversation.is_generating = True
|
||||||
|
conversation.save()
|
||||||
|
|
||||||
|
# Start AI processing in a background thread
|
||||||
|
thread = threading.Thread(target=run_ai_process_in_background, args=(conversation_id,))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'success', 'conversation_id': conversation_id})
|
||||||
|
return JsonResponse({'status': 'error', 'message': 'Text is required.'}, status=400)
|
||||||
|
|
||||||
|
conversations = Conversation.objects.order_by('-created_at')
|
||||||
|
selected_conversation = None
|
||||||
|
if conversation_id:
|
||||||
|
selected_conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||||
|
|
||||||
|
return render(request, 'core/chat.html', {
|
||||||
|
'conversation_list': conversations,
|
||||||
|
'selected_conversation': selected_conversation,
|
||||||
|
'timestamp': int(time.time()),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def conversation_list(request):
|
||||||
|
conversations = Conversation.objects.order_by('-created_at')
|
||||||
|
return render(request, 'core/conversation_list.html', {'conversation_list': conversations})
|
||||||
|
|
||||||
|
|
||||||
|
def get_conversation_messages(request, conversation_id):
|
||||||
|
conversation = get_object_or_404(Conversation, id=conversation_id)
|
||||||
|
messages = conversation.messages.order_by('created_at').values('sender', 'content', 'created_at')
|
||||||
|
return JsonResponse({
|
||||||
|
'messages': list(messages),
|
||||||
|
'is_generating': conversation.is_generating
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def settings_view(request):
|
||||||
|
custom_instructions, _ = Setting.objects.get_or_create(
|
||||||
|
key='custom_instructions',
|
||||||
|
defaults={'value': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
custom_instructions.value = request.POST.get('custom_instructions', '')
|
||||||
|
custom_instructions.save()
|
||||||
|
return redirect('core:settings')
|
||||||
|
|
||||||
|
return render(request, 'core/settings.html', {
|
||||||
|
'custom_instructions': custom_instructions
|
||||||
|
})
|
||||||
|
|||||||
@ -1,182 +1,297 @@
|
|||||||
/* custom.css */
|
/* General App Body & Layout */
|
||||||
|
|
||||||
:root {
|
|
||||||
--primary-color: #1A202C;
|
|
||||||
--secondary-color: #F7FAFC;
|
|
||||||
--accent-color: #4299E1;
|
|
||||||
--font-family-headings: 'Poppins', sans-serif;
|
|
||||||
--font-family-body: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-family-body);
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
|
background-color: #f8f9fa;
|
||||||
color: #333;
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
/* Main Chat Layout */
|
||||||
font-family: var(--font-family-headings);
|
.chat-container {
|
||||||
color: var(--primary-color);
|
display: flex;
|
||||||
}
|
height: calc(100vh - 120px); /* Adjusted for header/footer */
|
||||||
|
width: 100%;
|
||||||
.hero-section .display-4 {
|
background-color: #fff;
|
||||||
font-weight: 600;
|
border: 1px solid #dee2e6;
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section .lead {
|
|
||||||
color: #555;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #3182ce; /* A slightly darker shade of accent */
|
|
||||||
border-color: #2c73b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
/* Sidebar Styles */
|
||||||
border-color: var(--accent-color);
|
.chat-sidebar {
|
||||||
box-shadow: 0 0 0 0.25rem rgba(66, 153, 225, 0.25);
|
width: 280px;
|
||||||
}
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
.table {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4a5568;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border-bottom-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
padding: 0.4em 0.7em;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-todo {
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-inprogress {
|
|
||||||
background-color: #bee3f8;
|
|
||||||
color: #2c5282;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-blocked {
|
|
||||||
background-color: #fed7d7;
|
|
||||||
color: #9b2c2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-done {
|
|
||||||
background-color: #c6f6d5;
|
|
||||||
color: #2f855a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Kanban Board Styles */
|
|
||||||
.kanban-board-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 1.5rem;
|
|
||||||
background-color: #e9ecef; /* Light grey background for the container */
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanban-board {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-auto-columns: 280px; /* Fixed width for each column */
|
|
||||||
gap: 1.5rem;
|
|
||||||
padding-bottom: 1rem; /* For scrollbar spacing */
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanban-column {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 280px;
|
|
||||||
max-width: 300px;
|
|
||||||
background-color: #f7fafc;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-column .h5 {
|
.new-chat-form {
|
||||||
font-weight: 600;
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-cards {
|
.new-chat-form input {
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem 0 0 0.375rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form button {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #0d6efd;
|
||||||
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 60vh; /* Adjust as needed */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card {
|
.conversation-list a {
|
||||||
cursor: grab;
|
display: block;
|
||||||
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out;
|
padding: 0.75rem 1rem;
|
||||||
|
color: #495057;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card:hover {
|
.conversation-list a:hover {
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
background-color: #e9ecef;
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card .card-title {
|
.conversation-list a.active {
|
||||||
font-weight: 600;
|
background-color: #0d6efd;
|
||||||
color: #2d3748;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card .tags {
|
/* Main Content Area */
|
||||||
margin-top: 0.5rem;
|
.chat-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative; /* Needed for loader overlay */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chat Header */
|
||||||
|
.chat-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Area */
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai .message-content {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-author {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Input Form */
|
||||||
|
.chat-form-container {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State for Chat */
|
||||||
|
.no-conversation-selected {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader Styles */
|
||||||
.loader-overlay {
|
.loader-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
right: 0;
|
||||||
height: 100%;
|
bottom: 0;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
border: 8px solid #f3f3f3;
|
border: 5px solid #f3f3f3; /* Light grey */
|
||||||
border-top: 8px solid #3498db;
|
border-top: 5px solid #0d6efd; /* Blue */
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 60px;
|
width: 50px;
|
||||||
height: 60px;
|
height: 50px;
|
||||||
animation: spin 2s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* System and AI Command Messages */
|
||||||
|
.message.system .message-content {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card .card-body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-task-form {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Board Styles */
|
||||||
|
.kanban-board-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-board {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: max-content; /* Ensure board expands horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column {
|
||||||
|
flex: 1 1 300px; /* Flex-grow, flex-shrink, and basis */
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 320px;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100vh - 250px); /* Adjust based on your layout */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column h2 {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-cards {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 150px; /* Ensure drop zone is available even when empty */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card {
|
||||||
|
cursor: grab;
|
||||||
|
transition: background-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card .btn-close {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card:hover .btn-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For the drag-and-drop placeholder */
|
||||||
|
.sortable-ghost {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border: 2px dashed #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-drag {
|
||||||
|
opacity: 1 !important; /* Override Sortable.js default opacity */
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||||
|
transform: rotate(3deg);
|
||||||
|
}
|
||||||
|
|||||||
230
static/css/custom.css.bak
Normal file
230
static/css/custom.css.bak
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
/* General App Body & Layout */
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Chat Layout */
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 120px); /* Adjusted for header/footer */
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styles */
|
||||||
|
.chat-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem 0 0 0.375rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form button {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #0d6efd;
|
||||||
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #495057;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list a:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list a.active {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Area */
|
||||||
|
.chat-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative; /* Needed for loader overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Header */
|
||||||
|
.chat-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Area */
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai .message-content {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-author {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Input Form */
|
||||||
|
.chat-form-container {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State for Chat */
|
||||||
|
.no-conversation-selected {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader Styles */
|
||||||
|
.loader-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 5px solid #f3f3f3; /* Light grey */
|
||||||
|
border-top: 5px solid #0d6efd; /* Blue */
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System and AI Command Messages */
|
||||||
|
.message.system .message-content {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card .card-body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-task-form {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
@ -1,182 +1,297 @@
|
|||||||
/* custom.css */
|
/* General App Body & Layout */
|
||||||
|
|
||||||
:root {
|
|
||||||
--primary-color: #1A202C;
|
|
||||||
--secondary-color: #F7FAFC;
|
|
||||||
--accent-color: #4299E1;
|
|
||||||
--font-family-headings: 'Poppins', sans-serif;
|
|
||||||
--font-family-body: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-family-body);
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
|
background-color: #f8f9fa;
|
||||||
color: #333;
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
/* Main Chat Layout */
|
||||||
font-family: var(--font-family-headings);
|
.chat-container {
|
||||||
color: var(--primary-color);
|
display: flex;
|
||||||
}
|
height: calc(100vh - 120px); /* Adjusted for header/footer */
|
||||||
|
width: 100%;
|
||||||
.hero-section .display-4 {
|
background-color: #fff;
|
||||||
font-weight: 600;
|
border: 1px solid #dee2e6;
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section .lead {
|
|
||||||
color: #555;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #3182ce; /* A slightly darker shade of accent */
|
|
||||||
border-color: #2c73b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
/* Sidebar Styles */
|
||||||
border-color: var(--accent-color);
|
.chat-sidebar {
|
||||||
box-shadow: 0 0 0 0.25rem rgba(66, 153, 225, 0.25);
|
width: 280px;
|
||||||
}
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
.table {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4a5568;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
border-bottom-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
padding: 0.4em 0.7em;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-todo {
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-inprogress {
|
|
||||||
background-color: #bee3f8;
|
|
||||||
color: #2c5282;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-blocked {
|
|
||||||
background-color: #fed7d7;
|
|
||||||
color: #9b2c2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-done {
|
|
||||||
background-color: #c6f6d5;
|
|
||||||
color: #2f855a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Kanban Board Styles */
|
|
||||||
.kanban-board-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 1.5rem;
|
|
||||||
background-color: #e9ecef; /* Light grey background for the container */
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanban-board {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-auto-columns: 280px; /* Fixed width for each column */
|
|
||||||
gap: 1.5rem;
|
|
||||||
padding-bottom: 1rem; /* For scrollbar spacing */
|
|
||||||
}
|
|
||||||
|
|
||||||
.kanban-column {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 280px;
|
|
||||||
max-width: 300px;
|
|
||||||
background-color: #f7fafc;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-column .h5 {
|
.new-chat-form {
|
||||||
font-weight: 600;
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-cards {
|
.new-chat-form input {
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem 0 0 0.375rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form button {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #0d6efd;
|
||||||
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 60vh; /* Adjust as needed */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card {
|
.conversation-list a {
|
||||||
cursor: grab;
|
display: block;
|
||||||
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out;
|
padding: 0.75rem 1rem;
|
||||||
|
color: #495057;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card:hover {
|
.conversation-list a:hover {
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
background-color: #e9ecef;
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card .card-title {
|
.conversation-list a.active {
|
||||||
font-weight: 600;
|
background-color: #0d6efd;
|
||||||
color: #2d3748;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-card .tags {
|
/* Main Content Area */
|
||||||
margin-top: 0.5rem;
|
.chat-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative; /* Needed for loader overlay */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chat Header */
|
||||||
|
.chat-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Area */
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai .message-content {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-author {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Input Form */
|
||||||
|
.chat-form-container {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State for Chat */
|
||||||
|
.no-conversation-selected {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader Styles */
|
||||||
.loader-overlay {
|
.loader-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
right: 0;
|
||||||
height: 100%;
|
bottom: 0;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
border: 8px solid #f3f3f3;
|
border: 5px solid #f3f3f3; /* Light grey */
|
||||||
border-top: 8px solid #3498db;
|
border-top: 5px solid #0d6efd; /* Blue */
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 60px;
|
width: 50px;
|
||||||
height: 60px;
|
height: 50px;
|
||||||
animation: spin 2s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* System and AI Command Messages */
|
||||||
|
.message.system .message-content {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card .card-body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-task-form {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Board Styles */
|
||||||
|
.kanban-board-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-board {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: max-content; /* Ensure board expands horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column {
|
||||||
|
flex: 1 1 300px; /* Flex-grow, flex-shrink, and basis */
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 320px;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100vh - 250px); /* Adjust based on your layout */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column h2 {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-cards {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 150px; /* Ensure drop zone is available even when empty */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card {
|
||||||
|
cursor: grab;
|
||||||
|
transition: background-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card .btn-close {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card:hover .btn-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For the drag-and-drop placeholder */
|
||||||
|
.sortable-ghost {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border: 2px dashed #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-drag {
|
||||||
|
opacity: 1 !important; /* Override Sortable.js default opacity */
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||||
|
transform: rotate(3deg);
|
||||||
|
}
|
||||||
|
|||||||
230
staticfiles/css/custom.css.bak
Normal file
230
staticfiles/css/custom.css.bak
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
/* General App Body & Layout */
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Chat Layout */
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 120px); /* Adjusted for header/footer */
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styles */
|
||||||
|
.chat-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem 0 0 0.375rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chat-form button {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #0d6efd;
|
||||||
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #495057;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list a:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list a.active {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Area */
|
||||||
|
.chat-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative; /* Needed for loader overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Header */
|
||||||
|
.chat-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Area */
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai .message-content {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-author {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Input Form */
|
||||||
|
.chat-form-container {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State for Chat */
|
||||||
|
.no-conversation-selected {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader Styles */
|
||||||
|
.loader-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 5px solid #f3f3f3; /* Light grey */
|
||||||
|
border-top: 5px solid #0d6efd; /* Blue */
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System and AI Command Messages */
|
||||||
|
.message.system .message-content {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.ai_command .message-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card .card-body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-task-form {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user