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,
}
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 {
"success": False,
"status": status,
"error": error_message,
"response": decoded if decoded is not None else response_body,
"error": "AI proxy request failed",
"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'
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 .models import Article, TodoItem
from .models import Article, TodoItem, Setting, Conversation, Message
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):
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>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<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">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<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 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>
</ul>
</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 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>
<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 class="table-responsive">
<table class="table table-hover mb-0">
@ -50,6 +54,7 @@
<th scope="col">Tags</th>
<th scope="col">Status</th>
<th scope="col">Created</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@ -66,6 +71,12 @@
</td>
<td><span class="badge status-{{ item.status }}">{{ item.get_status_display }}</span></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>
{% empty %}
<tr>

View File

@ -25,6 +25,10 @@
{% for item in tasks %}
<div class="card kanban-card mb-3 shadow-sm" data-task-id="{{ item.id }}">
<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>
<p class="card-text small">{{ item.description|default:""|truncatewords:15 }}</p>
{% 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 . import views
from .views import index, article_detail, kanban_board, update_task_status
app_name = 'core'
urlpatterns = [
path("", index, name="index"),
path("kanban/", kanban_board, name="kanban_board"),
path("article/<int:article_id>/", article_detail, name="article_detail"),
path('update_task_status/', update_task_status, name='update_task_status'),
]
path("", views.index, name="index"),
path('kanban/', views.kanban_board, name='kanban'),
path('article/<int:article_id>/', views.article_detail, name='article_detail'),
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.views.decorators.http import require_POST
import json
from .models import Article, TodoItem
import logging
import threading
from .models import Article, TodoItem, Conversation, Message, Setting
from .forms import TodoItemForm
import time
from ai.local_ai_api import LocalAIApi
# Get an instance of a logger
logger = logging.getLogger(__name__)
def index(request):
if request.method == 'POST':
form = TodoItemForm(request.POST)
if form.is_valid():
form.save()
return redirect('index')
return redirect('core:index')
else:
form = TodoItemForm()
@ -26,6 +33,7 @@ def index(request):
}
return render(request, "core/index.html", context)
def kanban_board(request):
tasks = TodoItem.objects.all().order_by('created_at')
tasks_by_status = {
@ -40,10 +48,12 @@ def kanban_board(request):
}
return render(request, "core/kanban.html", context)
def article_detail(request, article_id):
article = Article.objects.get(pk=article_id)
return render(request, "core/article_detail.html", {"article": article})
@require_POST
def update_task_status(request):
try:
@ -57,4 +67,340 @@ def update_task_status(request):
return JsonResponse({'success': True})
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 */
:root {
--primary-color: #1A202C;
--secondary-color: #F7FAFC;
--accent-color: #4299E1;
--font-family-headings: 'Poppins', sans-serif;
--font-family-body: 'Inter', sans-serif;
}
/* General App Body & Layout */
body {
font-family: var(--font-family-body);
background: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f8f9fa;
color: #212529;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-headings);
color: var(--primary-color);
}
.hero-section .display-4 {
font-weight: 600;
}
.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 {
/* 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;
padding: 0.75rem 1rem;
}
.form-control:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 0.25rem rgba(66, 153, 225, 0.25);
}
.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);
/* Sidebar Styles */
.chat-sidebar {
width: 280px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
display: flex;
flex-direction: column;
padding: 1rem;
flex-shrink: 0;
}
.kanban-column .h5 {
font-weight: 600;
.new-chat-form {
display: flex;
margin-bottom: 1rem;
}
.kanban-cards {
flex-grow: 1;
.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;
max-height: 60vh; /* Adjust as needed */
}
.kanban-card {
cursor: grab;
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out;
.conversation-list a {
display: block;
padding: 0.75rem 1rem;
color: #495057;
text-decoration: none;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
}
.kanban-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-3px);
.conversation-list a:hover {
background-color: #e9ecef;
}
.kanban-card .card-title {
font-weight: 600;
color: #2d3748;
.conversation-list a.active {
background-color: #0d6efd;
color: #fff;
}
.kanban-card .tags {
margin-top: 0.5rem;
/* 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;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.7);
z-index: 1000;
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: 8px solid #f3f3f3;
border-top: 8px solid #3498db;
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #0d6efd; /* Blue */
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 2s linear infinite;
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;
}
/* 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 */
:root {
--primary-color: #1A202C;
--secondary-color: #F7FAFC;
--accent-color: #4299E1;
--font-family-headings: 'Poppins', sans-serif;
--font-family-body: 'Inter', sans-serif;
}
/* General App Body & Layout */
body {
font-family: var(--font-family-body);
background: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f8f9fa;
color: #212529;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-headings);
color: var(--primary-color);
}
.hero-section .display-4 {
font-weight: 600;
}
.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 {
/* 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;
padding: 0.75rem 1rem;
}
.form-control:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 0.25rem rgba(66, 153, 225, 0.25);
}
.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);
/* Sidebar Styles */
.chat-sidebar {
width: 280px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
display: flex;
flex-direction: column;
padding: 1rem;
flex-shrink: 0;
}
.kanban-column .h5 {
font-weight: 600;
.new-chat-form {
display: flex;
margin-bottom: 1rem;
}
.kanban-cards {
flex-grow: 1;
.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;
max-height: 60vh; /* Adjust as needed */
}
.kanban-card {
cursor: grab;
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out;
.conversation-list a {
display: block;
padding: 0.75rem 1rem;
color: #495057;
text-decoration: none;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
}
.kanban-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-3px);
.conversation-list a:hover {
background-color: #e9ecef;
}
.kanban-card .card-title {
font-weight: 600;
color: #2d3748;
.conversation-list a.active {
background-color: #0d6efd;
color: #fff;
}
.kanban-card .tags {
margin-top: 0.5rem;
/* 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;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.7);
z-index: 1000;
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: 8px solid #f3f3f3;
border-top: 8px solid #3498db;
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #0d6efd; /* Blue */
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 2s linear infinite;
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;
}
/* 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;
}