basic chat

This commit is contained in:
Flatlogic Bot 2025-11-19 22:52:43 +00:00
parent 330b813563
commit 864ef909c8
18 changed files with 722 additions and 351 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

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

@ -24,3 +24,19 @@ 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)
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()
is_from_user = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Message from {'User' if self.is_from_user else 'AI'} at {self.created_at}"

View File

@ -15,17 +15,20 @@
<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>
</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,82 @@
{% extends "base_chat.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_type }}">
<div class="message-content">
<div class="message-author">{% if message.sender_type == 'user' %}You{% else %}AI{% endif %}</div>
<p>{{ message.text|linebreaksbr }}</p>
</div>
</div>
{% endfor %}
</div>
<div class="chat-form-container">
<form method="post" action="{% url 'core:chat_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

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

@ -1,10 +1,13 @@
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('chat/', views.chat_view, name='chat'),
path('chat/<int:conversation_id>/', views.chat_view, name='chat_detail'),
]

View File

@ -2,9 +2,10 @@ 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 from .models import Article, TodoItem, Conversation, Message
from .forms import TodoItemForm from .forms import TodoItemForm
import time import time
from ai.local_ai_api import LocalAIApi
def index(request): def index(request):
if request.method == 'POST': if request.method == 'POST':
@ -57,4 +58,45 @@ 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)
def chat_view(request, conversation_id=None):
if request.method == 'POST':
if 'title' in request.POST:
title = request.POST.get('title', 'New Conversation')
conversation = Conversation.objects.create(title=title)
return redirect('core:chat_detail', conversation_id=conversation.id)
elif 'text' in request.POST and conversation_id:
text = request.POST.get('text')
selected_conversation = get_object_or_404(Conversation, id=conversation_id)
if text:
Message.objects.create(conversation=selected_conversation, text=text, sender_type='user')
try:
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": text}
]
})
ai_text = LocalAIApi.extract_text(response)
if not ai_text:
ai_text = "I couldn't process that. Please try again."
except Exception as e:
ai_text = f"An error occurred: {str(e)}"
Message.objects.create(conversation=selected_conversation, text=ai_text, sender_type='ai')
return redirect('core:chat_detail', conversation_id=selected_conversation.id)
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()),
})

View File

@ -1,182 +1,230 @@
/* custom.css */ /* General Body Styles */
: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;
} margin: 0;
height: 100vh;
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 {
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);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.kanban-column .h5 { /* Main Chat Layout */
font-weight: 600; .chat-container {
} display: flex;
height: 100vh;
.kanban-cards {
flex-grow: 1;
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;
}
.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);
}
.kanban-card .card-title {
font-weight: 600;
color: #2d3748;
}
.kanban-card .tags {
margin-top: 0.5rem;
}
.loader-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; overflow: hidden;
background: rgba(255, 255, 255, 0.7); background-color: #fff;
z-index: 1000; }
/* Sidebar Styles */
.chat-sidebar {
width: 280px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
display: flex;
flex-direction: column;
padding: 1.5rem;
}
.new-chat-form {
display: flex;
margin-bottom: 1.5rem;
}
.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;
background-color: #fff;
color: #495057;
}
.new-chat-form input:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.new-chat-form button {
padding: 0.75rem 1rem;
background-color: #007bff;
color: #fff;
border: 1px solid #007bff;
border-radius: 0 0.375rem 0.375rem 0;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
transition: background-color 0.2s;
}
.new-chat-form button:hover {
background-color: #0056b3;
}
.conversation-list {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
}
.conversation-list a {
display: block;
padding: 0.85rem 1.25rem;
color: #495057;
text-decoration: none;
border-radius: 0.375rem;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background-color 0.2s, color 0.2s;
}
.conversation-list a:hover {
background-color: #e9ecef;
color: #0056b3;
}
.conversation-list a.active {
background-color: #007bff;
color: #fff;
font-weight: 500;
}
/* Main Content Area */
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background-color: #ffffff;
}
/* Chat Header */
.chat-header {
padding: 1rem 2rem;
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.chat-header h3 {
margin: 0;
font-size: 1.25rem;
color: #343a40;
}
/* Message Area */
.chat-messages {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
.message {
margin-bottom: 1.5rem;
display: flex;
}
.message-content {
max-width: 75%;
padding: 1rem 1.5rem;
border-radius: 0.75rem;
line-height: 1.6;
position: relative;
}
.message.user {
justify-content: flex-end;
}
.message.user .message-content {
background-color: #007bff;
color: #fff;
border-radius: 0.75rem 0.75rem 0 0.75rem;
}
.message.ai .message-content {
background-color: #e9ecef;
color: #343a40;
border-radius: 0.75rem 0.75rem 0.75rem 0;
}
.message-author {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #6c757d;
}
.message.user .message-author {
color: rgba(255, 255, 255, 0.8);
}
/* Message Input Form */
.chat-form-container {
padding: 1.5rem 2rem;
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.chat-form {
display: flex;
align-items: stretch;
}
.chat-form textarea {
flex: 1;
padding: 1rem;
background-color: #fff;
color: #495057;
border: 1px solid #ced4da;
border-radius: 0.375rem;
font-size: 1rem;
line-height: 1.5;
resize: none;
min-height: 50px;
max-height: 200px;
transition: border-color 0.2s;
}
.chat-form textarea:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.chat-form button {
margin-left: 1rem;
padding: 0.75rem 1.5rem;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.chat-form button:hover {
background-color: #0056b3;
}
/* Empty State for Chat */
.no-conversation-selected {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%;
color: #6c757d;
text-align: center;
} }
.loader { .no-conversation-selected h2 {
border: 8px solid #f3f3f3; font-size: 2rem;
border-top: 8px solid #3498db; margin-bottom: 0.5rem;
border-radius: 50%; color: #343a40;
width: 60px;
height: 60px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
} }

View File

@ -1,182 +1,230 @@
/* custom.css */ /* General Body Styles */
: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;
} margin: 0;
height: 100vh;
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 {
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);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.kanban-column .h5 { /* Main Chat Layout */
font-weight: 600; .chat-container {
} display: flex;
height: 100vh;
.kanban-cards {
flex-grow: 1;
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;
}
.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);
}
.kanban-card .card-title {
font-weight: 600;
color: #2d3748;
}
.kanban-card .tags {
margin-top: 0.5rem;
}
.loader-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; overflow: hidden;
background: rgba(255, 255, 255, 0.7); background-color: #fff;
z-index: 1000; }
/* Sidebar Styles */
.chat-sidebar {
width: 280px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
display: flex;
flex-direction: column;
padding: 1.5rem;
}
.new-chat-form {
display: flex;
margin-bottom: 1.5rem;
}
.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;
background-color: #fff;
color: #495057;
}
.new-chat-form input:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.new-chat-form button {
padding: 0.75rem 1rem;
background-color: #007bff;
color: #fff;
border: 1px solid #007bff;
border-radius: 0 0.375rem 0.375rem 0;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
transition: background-color 0.2s;
}
.new-chat-form button:hover {
background-color: #0056b3;
}
.conversation-list {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
}
.conversation-list a {
display: block;
padding: 0.85rem 1.25rem;
color: #495057;
text-decoration: none;
border-radius: 0.375rem;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background-color 0.2s, color 0.2s;
}
.conversation-list a:hover {
background-color: #e9ecef;
color: #0056b3;
}
.conversation-list a.active {
background-color: #007bff;
color: #fff;
font-weight: 500;
}
/* Main Content Area */
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background-color: #ffffff;
}
/* Chat Header */
.chat-header {
padding: 1rem 2rem;
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.chat-header h3 {
margin: 0;
font-size: 1.25rem;
color: #343a40;
}
/* Message Area */
.chat-messages {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
.message {
margin-bottom: 1.5rem;
display: flex;
}
.message-content {
max-width: 75%;
padding: 1rem 1.5rem;
border-radius: 0.75rem;
line-height: 1.6;
position: relative;
}
.message.user {
justify-content: flex-end;
}
.message.user .message-content {
background-color: #007bff;
color: #fff;
border-radius: 0.75rem 0.75rem 0 0.75rem;
}
.message.ai .message-content {
background-color: #e9ecef;
color: #343a40;
border-radius: 0.75rem 0.75rem 0.75rem 0;
}
.message-author {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #6c757d;
}
.message.user .message-author {
color: rgba(255, 255, 255, 0.8);
}
/* Message Input Form */
.chat-form-container {
padding: 1.5rem 2rem;
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.chat-form {
display: flex;
align-items: stretch;
}
.chat-form textarea {
flex: 1;
padding: 1rem;
background-color: #fff;
color: #495057;
border: 1px solid #ced4da;
border-radius: 0.375rem;
font-size: 1rem;
line-height: 1.5;
resize: none;
min-height: 50px;
max-height: 200px;
transition: border-color 0.2s;
}
.chat-form textarea:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.chat-form button {
margin-left: 1rem;
padding: 0.75rem 1.5rem;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.chat-form button:hover {
background-color: #0056b3;
}
/* Empty State for Chat */
.no-conversation-selected {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%;
color: #6c757d;
text-align: center;
} }
.loader { .no-conversation-selected h2 {
border: 8px solid #f3f3f3; font-size: 2rem;
border-top: 8px solid #3498db; margin-bottom: 0.5rem;
border-radius: 50%; color: #343a40;
width: 60px;
height: 60px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
} }