Compare commits

..

10 Commits

Author SHA1 Message Date
705b35bbc3 increase iterations count to 31 2025-11-20 19:52:18 +00:00
Flatlogic Bot
63cf731d1f pooling in chat! 2025-11-20 19:43:30 +00:00
Flatlogic Bot
4c1df17a46 async AI job 2025-11-20 19:22:48 +00:00
Flatlogic Bot
1566d6c207 custom instructions 2025-11-20 17:15:00 +00:00
Flatlogic Bot
e2769f2d2d ai agent! 2025-11-20 00:53:41 +00:00
Flatlogic Bot
3b7559b157 basic AI agent 2025-11-20 00:44:39 +00:00
Flatlogic Bot
b700e16dec feed tasks to AI 2025-11-19 23:38:56 +00:00
Flatlogic Bot
94ac62d585 chat with history 2025-11-19 23:06:44 +00:00
Flatlogic Bot
e4f2931b77 basic chat 2025-11-19 23:02:45 +00:00
Flatlogic Bot
864ef909c8 basic chat 2025-11-19 22:52:43 +00:00
37 changed files with 1905 additions and 307 deletions

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -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',
},
}

View File

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

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

View File

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

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

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

View File

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

View File

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

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

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

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

View File

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

View File

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

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

View File

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

View File

@ -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:
@ -57,4 +67,340 @@ 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
})

View File

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

View File

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

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