diff --git a/CHATBOT_TEXT_ENDPOINT_GUIDE.md b/IA_docs/CHATBOT_TEXT_ENDPOINT_GUIDE.md similarity index 100% rename from CHATBOT_TEXT_ENDPOINT_GUIDE.md rename to IA_docs/CHATBOT_TEXT_ENDPOINT_GUIDE.md diff --git a/IA_docs/CHAT_HISTORY_API.md b/IA_docs/CHAT_HISTORY_API.md new file mode 100644 index 0000000..86823ef --- /dev/null +++ b/IA_docs/CHAT_HISTORY_API.md @@ -0,0 +1,1009 @@ +# Chat History API - Documentazione Completa + +## Indice +1. [Panoramica](#panoramica) +2. [Architettura](#architettura) +3. [Modelli Database](#modelli-database) +4. [Endpoints API](#endpoints-api) +5. [Esempi di Utilizzo](#esempi-di-utilizzo) +6. [Gestione degli Errori](#gestione-degli-errori) +7. [Limiti e Best Practices](#limiti-e-best-practices) + +--- + +## Panoramica + +Il sistema di **Chat History** di MyTaskly permette agli utenti di gestire sessioni di chat persistenti con l'assistente AI. Ogni utente può: + +- Creare e gestire più sessioni di chat +- Mantenere la cronologia dei messaggi +- Organizzare le chat con titoli personalizzati +- Pinnare le chat più importanti +- Generare automaticamente titoli significativi +- Eliminare chat non più necessarie + +**Base URL:** `http://localhost:8080/chat/history` + +**Autenticazione:** Tutti gli endpoint richiedono: +- Header `X-API-Key`: Chiave API configurata nel server +- Header `Authorization: Bearer {token}`: Token JWT ottenuto dal login + +--- + +## Architettura + +### Componenti Principali + +``` +┌─────────────────────────────────────────────────────────────┐ +│ CLIENT APPLICATION │ +└────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ API Routes (chat_history.py) │ +│ POST / GET / PATCH /{id} DELETE /{id} │ +└────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Service Layer (chat_history_service.py) │ +│ - create_new_chat() - list_user_chats() │ +│ - add_message_to_chat() - update_chat_title() │ +│ - get_chat_history() - toggle_chat_pin() │ +│ - generate_chat_title() - delete_chat() │ +└────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CRUD Layer (chat_session & chat_message) │ +│ - create_chat_session() - get_user_chat_sessions() │ +│ - create_chat_message() - update_chat_session() │ +│ - get_chat_session() - delete_chat_session() │ +└────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Database (PostgreSQL) │ +│ ChatSession Table │ ChatMessage Table │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Flusso di Creazione Titolo Automatico + +``` +User Message → add_message_to_chat() → Primo Messaggio? + │ + ▼ Sì + generate_chat_title() + │ + ▼ + OpenAI API + (gpt-4o-mini) + │ + ▼ + Titolo Generato (max 50 char) + │ + ▼ + update_chat_session() +``` + +--- + +## Modelli Database + +### ChatSession + +Rappresenta una sessione di chat completa. + +**Tabella:** `chat_sessions` + +| Campo | Tipo | Descrizione | +|-----------------------|------------|--------------------------------------------------| +| `chat_id` | VARCHAR(50)| ID univoco della chat (PK) | +| `user_id` | INT | ID utente proprietario (FK → users.user_id) | +| `title` | VARCHAR(100)| Titolo della chat | +| `is_pinned` | BOOLEAN | Chat pinnata in alto (default: false) | +| `created_at` | TIMESTAMP | Data/ora creazione | +| `updated_at` | TIMESTAMP | Data/ora ultimo aggiornamento | +| `message_count` | INT | Numero totale messaggi (default: 0) | +| `last_message_preview`| TEXT | Anteprima ultimo messaggio (max 200 char) | + +**Indici:** +- `idx_chat_user`: (user_id, updated_at DESC) - ottimizza listing per utente +- `idx_chat_pinned`: (user_id, is_pinned, updated_at DESC) - ottimizza filtro pinnati + +**Relazioni:** +- `user`: Relazione ForeignKey verso Users +- `messages`: Relazione OneToMany verso ChatMessage + +### ChatMessage + +Rappresenta un singolo messaggio in una chat. + +**Tabella:** `chat_messages` + +| Campo | Tipo | Descrizione | +|--------------|--------------|------------------------------------------------| +| `message_id` | INT | ID auto-incrementale (PK) | +| `chat_id` | VARCHAR(50) | ID chat di appartenenza (FK → chat_sessions) | +| `role` | VARCHAR(20) | Ruolo: 'user', 'assistant', 'system' | +| `content` | TEXT | Contenuto del messaggio | +| `created_at` | TIMESTAMP | Data/ora creazione | +| `token_count`| INT | Numero token (opzionale) | +| `model` | VARCHAR(50) | Modello AI usato (per assistant, opzionale) | +| `tool_name` | VARCHAR(100) | Nome tool MCP chiamato (opzionale) | +| `tool_input` | JSONB | Input del tool (opzionale) | +| `tool_output`| JSONB | Output del tool (opzionale) | + +**Indici:** +- `idx_message_chat`: (chat_id, created_at ASC) - ottimizza recupero cronologico +- `idx_message_role`: (chat_id, role) - ottimizza ricerca per ruolo + +**Relazioni:** +- `chat_session`: Relazione ForeignKey verso ChatSession (CASCADE on delete) + +--- + +## Endpoints API + +### 1. Crea Nuova Chat Session + +Crea una nuova sessione di chat per l'utente autenticato. + +**Endpoint:** `POST /chat/history/` + +**Headers:** +```http +Authorization: Bearer {access_token} +X-API-Key: {api_key} +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "chat_id": "optional_custom_id" // Opzionale: ID personalizzato +} +``` + +**Response:** `201 Created` +```json +{ + "chat_id": "chat_427649acde81", + "user_id": 1, + "title": "New Chat", + "is_pinned": false, + "created_at": "2026-01-17T05:43:53.562796Z", + "updated_at": "2026-01-17T05:43:53.562796Z", + "message_count": 0, + "last_message_preview": null +} +``` + +**Note:** +- Se `chat_id` non viene fornito, viene generato automaticamente (formato: `chat_{12_hex_chars}`) +- Il titolo iniziale è "New Chat", verrà aggiornato automaticamente al primo messaggio utente +- Ogni utente può creare fino a 100 chat (configurabile) + +--- + +### 2. Lista Sessioni Chat + +Recupera tutte le sessioni di chat dell'utente autenticato con paginazione. + +**Endpoint:** `GET /chat/history/` + +**Headers:** +```http +Authorization: Bearer {access_token} +X-API-Key: {api_key} +``` + +**Query Parameters:** +- `skip` (int, default=0): Numero di record da saltare (paginazione) +- `limit` (int, default=50, max=100): Numero massimo di record da restituire +- `pinned_only` (bool, default=false): Se true, restituisce solo chat pinnate + +**Esempi:** +```http +GET /chat/history/?skip=0&limit=20 +GET /chat/history/?pinned_only=true +GET /chat/history/?skip=10&limit=5&pinned_only=false +``` + +**Response:** `200 OK` +```json +{ + "total": 3, + "chats": [ + { + "chat_id": "chat_f63a", + "user_id": 1, + "title": "AI assistant for your tasks", + "is_pinned": true, + "created_at": "2026-01-08T09:20:00Z", + "updated_at": "2026-01-15T11:30:00Z", + "message_count": 42, + "last_message_preview": "I've created 3 new tasks for you..." + }, + { + "chat_id": "chat_arcv", + "user_id": 1, + "title": "Manage task with me", + "is_pinned": false, + "created_at": "2026-01-10T10:30:00Z", + "updated_at": "2026-01-16T15:45:00Z", + "message_count": 15, + "last_message_preview": "Sure, I've updated your task for tomorrow..." + } + ] +} +``` + +**Ordinamento:** +- Chat pinnate appaiono sempre per prime +- All'interno di ciascun gruppo (pinnate/non pinnate): ordine per `updated_at` DESC (più recente prima) + +--- + +### 3. Recupera Chat con Messaggi + +Recupera una specifica chat con tutti i suoi messaggi. + +**Endpoint:** `GET /chat/history/{chat_id}` + +**Headers:** +```http +Authorization: Bearer {access_token} +X-API-Key: {api_key} +``` + +**Path Parameters:** +- `chat_id` (string): ID della chat da recuperare + +**Response:** `200 OK` +```json +{ + "chat_id": "chat_f63a", + "user_id": 1, + "title": "Task Management Discussion", + "is_pinned": true, + "created_at": "2026-01-08T09:20:00Z", + "updated_at": "2026-01-15T11:30:00Z", + "message_count": 3, + "last_message_preview": "I've created the task for tomorrow.", + "messages": [ + { + "message_id": 1, + "chat_id": "chat_f63a", + "role": "user", + "content": "Create a task for tomorrow at 10 AM", + "created_at": "2026-01-15T11:25:00Z", + "token_count": 12, + "model": null, + "tool_name": null, + "tool_input": null, + "tool_output": null + }, + { + "message_id": 2, + "chat_id": "chat_f63a", + "role": "assistant", + "content": "I've created the task for tomorrow at 10 AM.", + "created_at": "2026-01-15T11:30:00Z", + "token_count": 15, + "model": "gpt-4", + "tool_name": "create_task", + "tool_input": { + "title": "Meeting", + "start_time": "2026-01-16T10:00:00Z" + }, + "tool_output": { + "task_id": 123, + "status": "success" + } + } + ] +} +``` + +**Errori:** +- `404 Not Found`: Chat non trovata o non appartiene all'utente + +--- + +### 4. Aggiorna Chat (Titolo o Pin) + +Aggiorna il titolo o lo stato di pin di una chat. + +**Endpoint:** `PATCH /chat/history/{chat_id}` + +**Headers:** +```http +Authorization: Bearer {access_token} +X-API-Key: {api_key} +Content-Type: application/json +``` + +**Path Parameters:** +- `chat_id` (string): ID della chat da aggiornare + +**Request Body (Aggiorna Titolo):** +```json +{ + "title": "New Custom Title" +} +``` + +**Request Body (Pin/Unpin):** +```json +{ + "is_pinned": true +} +``` + +**Response:** `200 OK` +```json +{ + "chat_id": "chat_f63a", + "user_id": 1, + "title": "New Custom Title", + "is_pinned": false, + "created_at": "2026-01-08T09:20:00Z", + "updated_at": "2026-01-17T10:15:00Z", + "message_count": 42, + "last_message_preview": "I've created 3 new tasks for you..." +} +``` + +**Validazione:** +- `title`: max 100 caratteri +- `is_pinned`: booleano true/false +- Almeno un campo deve essere fornito + +**Errori:** +- `400 Bad Request`: Nessun campo fornito per l'aggiornamento +- `404 Not Found`: Chat non trovata o non appartiene all'utente + +--- + +### 5. Elimina Chat + +Elimina permanentemente una chat e tutti i suoi messaggi. + +**Endpoint:** `DELETE /chat/history/{chat_id}` + +**Headers:** +```http +Authorization: Bearer {access_token} +X-API-Key: {api_key} +``` + +**Path Parameters:** +- `chat_id` (string): ID della chat da eliminare + +**Response:** `204 No Content` + +**Note:** +- L'eliminazione è **permanente** e **irreversibile** +- Tutti i messaggi associati vengono eliminati automaticamente (CASCADE) +- Non è possibile recuperare una chat eliminata + +**Errori:** +- `404 Not Found`: Chat non trovata o non appartiene all'utente + +--- + +## Esempi di Utilizzo + +### Scenario 1: Creazione e Utilizzo Chat Completa + +```python +import requests + +BASE_URL = "http://localhost:8080" +API_KEY = "abc123" + +# 1. Login utente +login_response = requests.post( + f"{BASE_URL}/auth/login", + json={"username": "TheAdmin", "password": "Ciao"}, + headers={"X-API-Key": API_KEY} +) +token = login_response.json()["bearer_token"] + +headers = { + "Authorization": f"Bearer {token}", + "X-API-Key": API_KEY, + "Content-Type": "application/json" +} + +# 2. Crea nuova chat +chat_response = requests.post( + f"{BASE_URL}/chat/history/", + headers=headers, + json={} # ID generato automaticamente +) +chat_id = chat_response.json()["chat_id"] +print(f"Chat creata: {chat_id}") + +# 3. Invia primo messaggio (genera titolo automatico) +# Questo avverrebbe tramite l'endpoint chatbot, esempio: +message_response = requests.post( + f"{BASE_URL}/chat/text", + headers=headers, + json={ + "quest": "Come posso creare un task per domani?", + "chat_id": chat_id + } +) + +# 4. Recupera chat aggiornata +chat_details = requests.get( + f"{BASE_URL}/chat/history/{chat_id}", + headers=headers +) +print(f"Titolo generato: {chat_details.json()['title']}") +print(f"Messaggi: {len(chat_details.json()['messages'])}") + +# 5. Pinna la chat +requests.patch( + f"{BASE_URL}/chat/history/{chat_id}", + headers=headers, + json={"is_pinned": True} +) + +# 6. Lista tutte le chat (pinnate in cima) +all_chats = requests.get( + f"{BASE_URL}/chat/history/?limit=20", + headers=headers +) +print(f"Totale chat: {all_chats.json()['total']}") +``` + +### Scenario 2: Gestione Chat Multiple + +```python +# Lista solo chat pinnate +pinned_chats = requests.get( + f"{BASE_URL}/chat/history/?pinned_only=true", + headers=headers +) + +for chat in pinned_chats.json()['chats']: + print(f"📌 {chat['title']} - {chat['message_count']} messaggi") + +# Paginazione: seconda pagina (10 record per pagina) +page_2 = requests.get( + f"{BASE_URL}/chat/history/?skip=10&limit=10", + headers=headers +) + +# Aggiorna titolo di una chat specifica +requests.patch( + f"{BASE_URL}/chat/history/chat_abc123", + headers=headers, + json={"title": "Progetto Marketing - Discussione"} +) + +# Elimina chat vecchia +requests.delete( + f"{BASE_URL}/chat/history/chat_old_id", + headers=headers +) +``` + +### Scenario 3: Integrazione Frontend React + +```javascript +// chatHistoryService.js +const API_BASE = 'http://localhost:8080'; +const API_KEY = 'abc123'; + +class ChatHistoryService { + constructor(authToken) { + this.headers = { + 'Authorization': `Bearer ${authToken}`, + 'X-API-Key': API_KEY, + 'Content-Type': 'application/json' + }; + } + + async createChat(customId = null) { + const response = await fetch(`${API_BASE}/chat/history/`, { + method: 'POST', + headers: this.headers, + body: JSON.stringify({ chat_id: customId }) + }); + return response.json(); + } + + async listChats(options = {}) { + const { skip = 0, limit = 50, pinnedOnly = false } = options; + const params = new URLSearchParams({ + skip, + limit, + pinned_only: pinnedOnly + }); + + const response = await fetch( + `${API_BASE}/chat/history/?${params}`, + { headers: this.headers } + ); + return response.json(); + } + + async getChat(chatId) { + const response = await fetch( + `${API_BASE}/chat/history/${chatId}`, + { headers: this.headers } + ); + + if (!response.ok) { + throw new Error('Chat not found'); + } + + return response.json(); + } + + async updateTitle(chatId, newTitle) { + const response = await fetch( + `${API_BASE}/chat/history/${chatId}`, + { + method: 'PATCH', + headers: this.headers, + body: JSON.stringify({ title: newTitle }) + } + ); + return response.json(); + } + + async togglePin(chatId, isPinned) { + const response = await fetch( + `${API_BASE}/chat/history/${chatId}`, + { + method: 'PATCH', + headers: this.headers, + body: JSON.stringify({ is_pinned: isPinned }) + } + ); + return response.json(); + } + + async deleteChat(chatId) { + const response = await fetch( + `${API_BASE}/chat/history/${chatId}`, + { + method: 'DELETE', + headers: this.headers + } + ); + + return response.ok; + } +} + +// Utilizzo +const chatService = new ChatHistoryService(userToken); + +// Carica lista chat +const { chats, total } = await chatService.listChats({ limit: 20 }); + +// Crea nuova chat +const newChat = await chatService.createChat(); + +// Pinna/Spinna chat +await chatService.togglePin('chat_abc123', true); +``` + +--- + +## Gestione degli Errori + +### Codici di Stato HTTP + +| Codice | Significato | Quando Appare | +|--------|-------------|---------------| +| `200` | OK | Richiesta completata con successo | +| `201` | Created | Chat creata con successo | +| `204` | No Content | Chat eliminata con successo | +| `400` | Bad Request | Parametri invalidi o mancanti | +| `401` | Unauthorized | Token mancante o scaduto | +| `403` | Forbidden | API Key mancante o invalida | +| `404` | Not Found | Chat non trovata | +| `500` | Internal Server Error | Errore del server | + +### Formato Errori + +Gli errori seguono il formato standard FastAPI: + +```json +{ + "detail": "Chat session not found" +} +``` + +### Esempi di Errori Comuni + +**1. Token Scaduto (401)** +```json +{ + "detail": "Token has expired" +} +``` + +**Soluzione:** Effettuare un nuovo login per ottenere un nuovo token. + +**2. Chat Non Trovata (404)** +```json +{ + "detail": "Chat session not found" +} +``` + +**Possibili Cause:** +- `chat_id` inesistente +- Chat appartiene a un altro utente +- Chat eliminata + +**3. Parametri Invalidi (400)** +```json +{ + "detail": "No updates provided" +} +``` + +**Soluzione:** Fornire almeno `title` o `is_pinned` nella richiesta PATCH. + +**4. Limite Paginazione Superato (422)** +```json +{ + "detail": [ + { + "loc": ["query", "limit"], + "msg": "ensure this value is less than or equal to 100", + "type": "value_error" + } + ] +} +``` + +**Soluzione:** Utilizzare `limit` ≤ 100. + +--- + +## Limiti e Best Practices + +### Limiti di Sistema + +| Risorsa | Limite | Configurabile | +|---------|--------|---------------| +| Chat per utente | 100 | Sì (`MAX_CHATS_PER_USER`) | +| Messaggi per chat | Illimitato | No | +| Lunghezza titolo | 100 caratteri | No | +| Lunghezza anteprima | 200 caratteri | No | +| Risultati paginazione | 100 per pagina | No | +| Lunghezza messaggio | Illimitato | No | +| Chat pinnate | Illimitato | No | + +### Best Practices + +#### 1. Gestione Token di Autenticazione + +```python +# ❌ NON FARE - Token hardcoded +headers = {"Authorization": "Bearer eyJhbGc..."} + +# ✅ FARE - Token da variabile ambiente o storage sicuro +import os +token = os.getenv("USER_AUTH_TOKEN") +headers = {"Authorization": f"Bearer {token}"} +``` + +#### 2. Paginazione Efficiente + +```python +# ❌ NON FARE - Caricare tutte le chat +all_chats = requests.get(f"{BASE_URL}/chat/history/?limit=100") + +# ✅ FARE - Paginazione a piccoli chunk +def get_all_chats_paginated(): + all_chats = [] + skip = 0 + limit = 20 + + while True: + response = requests.get( + f"{BASE_URL}/chat/history/?skip={skip}&limit={limit}" + ) + data = response.json() + all_chats.extend(data['chats']) + + if len(data['chats']) < limit: + break + + skip += limit + + return all_chats +``` + +#### 3. Gestione Errori Robusta + +```python +# ✅ FARE - Gestione errori completa +def get_chat_safe(chat_id): + try: + response = requests.get( + f"{BASE_URL}/chat/history/{chat_id}", + headers=headers, + timeout=10 # Timeout per evitare blocchi + ) + response.raise_for_status() # Solleva eccezione per status >= 400 + return response.json() + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + print(f"Chat {chat_id} non trovata") + return None + elif e.response.status_code == 401: + print("Token scaduto, effettua nuovo login") + # Trigger re-login + return None + else: + print(f"Errore HTTP: {e}") + raise + + except requests.exceptions.Timeout: + print("Timeout connessione") + return None + + except requests.exceptions.RequestException as e: + print(f"Errore di rete: {e}") + return None +``` + +#### 4. Caching Lato Client + +```python +# ✅ FARE - Implementare cache per ridurre chiamate API +from datetime import datetime, timedelta + +class ChatCache: + def __init__(self, ttl_seconds=300): # 5 minuti + self.cache = {} + self.ttl = timedelta(seconds=ttl_seconds) + + def get(self, chat_id): + if chat_id in self.cache: + data, timestamp = self.cache[chat_id] + if datetime.now() - timestamp < self.ttl: + return data + return None + + def set(self, chat_id, data): + self.cache[chat_id] = (data, datetime.now()) + + def invalidate(self, chat_id): + if chat_id in self.cache: + del self.cache[chat_id] + +# Utilizzo +cache = ChatCache() + +def get_chat_cached(chat_id): + cached = cache.get(chat_id) + if cached: + return cached + + data = get_chat_safe(chat_id) + if data: + cache.set(chat_id, data) + return data +``` + +#### 5. Ottimizzazione UI + +```javascript +// ✅ FARE - Implementare infinite scroll invece di paginazione manuale +class ChatListManager { + constructor(chatService) { + this.chatService = chatService; + this.chats = []; + this.skip = 0; + this.limit = 20; + this.hasMore = true; + } + + async loadMore() { + if (!this.hasMore) return; + + const { chats, total } = await this.chatService.listChats({ + skip: this.skip, + limit: this.limit + }); + + this.chats.push(...chats); + this.skip += this.limit; + this.hasMore = this.chats.length < total; + + return chats; + } + + async refresh() { + this.chats = []; + this.skip = 0; + this.hasMore = true; + return this.loadMore(); + } +} +``` + +#### 6. Sincronizzazione Titoli Automatici + +```python +# ✅ FARE - Attendere generazione titolo dopo primo messaggio +import time + +def create_chat_and_send_message(message): + # Crea chat + chat = create_chat() + chat_id = chat['chat_id'] + + # Invia primo messaggio + send_message(chat_id, message) + + # Attendi brevemente per generazione titolo + time.sleep(2) + + # Recupera chat con titolo aggiornato + updated_chat = get_chat(chat_id) + + return updated_chat +``` + +#### 7. Validazione Input Lato Client + +```javascript +// ✅ FARE - Validare prima di inviare al server +function validateChatTitle(title) { + if (!title || title.trim().length === 0) { + throw new Error('Title cannot be empty'); + } + + if (title.length > 100) { + throw new Error('Title must be 100 characters or less'); + } + + return title.trim(); +} + +async function updateChatTitle(chatId, newTitle) { + try { + const validTitle = validateChatTitle(newTitle); + return await chatService.updateTitle(chatId, validTitle); + } catch (error) { + console.error('Validation error:', error.message); + // Show error to user + } +} +``` + +### Performance Tips + +1. **Usa `pinned_only=true` quando appropriato** - Riduce il carico quando si cercano solo chat importanti + +2. **Implementa debouncing per ricerche** - Non fare una chiamata API per ogni keystroke durante la ricerca + +3. **Prefetch chat pinnate** - Carica le chat pinnate all'avvio dell'app per accesso rapido + +4. **Lazy loading messaggi** - Carica i messaggi solo quando l'utente apre una chat specifica + +5. **Batch operations** - Se devi aggiornare più chat, considera di raggruppare le operazioni + +### Sicurezza + +1. **Mai esporre API Key nel frontend** - Usa variabili ambiente o proxy backend + +2. **Validare sempre lato server** - Non fidarsi mai dei dati client + +3. **Implementare rate limiting** - Previeni abusi (già implementato nel server) + +4. **Token refresh** - Implementa logica per refresh automatico token prima della scadenza + +--- + +## Note sulla Generazione Titoli + +### Modello Utilizzato + +- **Modello:** `gpt-4o-mini` +- **Max Tokens:** 20 +- **Temperature:** 0.7 +- **Costo:** ~$0.0001 per titolo generato + +### Prompt di Sistema + +``` +You are a title generator. Generate a concise, descriptive title +for a chat conversation based on the user's first message. +The title should be maximum 50 characters, in the same language +as the user's message. Return ONLY the title, without quotes or +extra text. +``` + +### Esempi di Titoli Generati + +| Messaggio Utente | Titolo Generato | +|------------------|-----------------| +| "Come posso creare un task per domani?" | "Creazione task per domani" | +| "What's the weather like today in Rome?" | "Weather in Rome today" | +| "Ricordami di comprare il latte domani alle 10" | "Reminder: comprare latte domani" | +| "Can you explain how machine learning works?" | "Machine Learning Explanation" | + +### Fallback Strategy + +Se la generazione titolo fallisce (API down, errore, timeout): +- **Fallback:** Primi 47 caratteri del messaggio + "..." +- **Esempio:** "This is a very long message that will be trun..." + +--- + +## FAQ + +### Q: Posso creare chat con ID personalizzati? +**R:** Sì, puoi fornire un `chat_id` custom nella richiesta di creazione. Se non fornito, viene generato automaticamente nel formato `chat_{12_hex_chars}`. + +### Q: Cosa succede ai messaggi quando elimino una chat? +**R:** Tutti i messaggi associati vengono eliminati automaticamente grazie al CASCADE nella relazione database. L'eliminazione è permanente. + +### Q: Posso modificare un messaggio già inviato? +**R:** No, i messaggi sono immutabili. Questa è una scelta di design per mantenere l'integrità della cronologia conversazionale. + +### Q: Quante chat pinnate posso avere? +**R:** Non c'è limite al numero di chat pinnate. Tuttavia, per UX ottimale, si consiglia di pinnare solo le chat più importanti (3-5). + +### Q: Il titolo generato automaticamente può essere cambiato? +**R:** Sì, puoi sempre aggiornare il titolo manualmente tramite l'endpoint PATCH, sovrascrivendo quello generato automaticamente. + +### Q: Come funziona l'ordinamento delle chat? +**R:** Le chat sono ordinate per: +1. **Pinnate prima** (is_pinned=true in cima) +2. **All'interno di ogni gruppo:** `updated_at` DESC (più recenti prima) + +### Q: Cosa succede se supero il limite di 100 chat? +**R:** La creazione di nuove chat verrà bloccata. Dovrai eliminare chat vecchie per liberare spazio. + +### Q: I messaggi con tool calls vengono salvati? +**R:** Sì, i messaggi con chiamate MCP tools includono `tool_name`, `tool_input` e `tool_output` per tracciare l'intero flusso conversazionale. + +--- + +## Changelog + +### v1.0.0 (2026-01-17) +- ✅ Implementazione completa sistema Chat History +- ✅ CRUD operations per chat sessions +- ✅ Gestione messaggi con metadata tool calls +- ✅ Generazione automatica titoli con GPT-4o-mini +- ✅ Sistema pin/unpin chat +- ✅ Paginazione con ordinamento intelligente +- ✅ Cascade deletion per messaggi +- ✅ Validazione e gestione errori +- ✅ Test suite completa (19/19 test passed) +- ✅ Documentazione API completa + +--- + +## Supporto + +Per problemi o domande: +- **Issues:** GitHub repository +- **Email:** support@mytasklyapp.com +- **Documentazione tecnica:** `/docs` endpoint Swagger UI + +--- + +**Versione Documentazione:** 1.0.0 +**Ultimo Aggiornamento:** 2026-01-17 +**Autore:** MyTaskly Team diff --git a/IA_docs/CHAT_SSE_STREAMING.md b/IA_docs/CHAT_SSE_STREAMING.md new file mode 100644 index 0000000..548aee0 --- /dev/null +++ b/IA_docs/CHAT_SSE_STREAMING.md @@ -0,0 +1,279 @@ +# Chat SSE Streaming - Documentazione + +## Panoramica + +L'endpoint `/chat/text` utilizza Server-Sent Events (SSE) per inviare risposte in streaming dal server al client. Il sistema è stato aggiornato per gestire la cronologia delle chat lato server tramite `chat_id`, eliminando la necessità di inviare l'intera cronologia ad ogni richiesta. + +## Cambiamenti Implementati + +### Prima (Vecchio Sistema) +```typescript +// Il client inviava tutta la cronologia messaggi +await sendMessageToBot( + userMessage, + modelType, + previousMessages, // ❌ Tutta la cronologia inviata dal client + onStreamChunk, + chatId +); +``` + +### Dopo (Nuovo Sistema) +```typescript +// Il client invia solo il chat_id per identificare la sessione +await sendMessageToBot( + userMessage, + modelType, + onStreamChunk, + chatId // ✅ Solo il chat_id per identificare la chat +); +``` + +## Formato Request + +### Endpoint +``` +POST /chat/text +Content-Type: application/json +Authorization: Bearer {token} +``` + +### Body +```json +{ + "quest": "Il tuo messaggio qui", + "model": "base" | "advanced", + "chat_id": "uuid-della-chat-opzionale" +} +``` + +**Parametri:** +- `quest` (required): Il messaggio dell'utente +- `model` (optional): Tipo di modello ("base" o "advanced", default: "base") +- `chat_id` (optional): ID della chat per continuare una conversazione esistente + +## Formato Response (SSE) + +Il server invia eventi Server-Sent Events con i seguenti tipi: + +### 1. Chat Info Event +Inviato all'inizio dello stream per comunicare il `chat_id` della sessione. + +``` +data: {"type": "chat_info", "chat_id": "550e8400-e29b-41d4-a716-446655440000", "is_new": true} +``` + +**Campi:** +- `type`: "chat_info" +- `chat_id`: UUID della sessione di chat +- `is_new`: `true` se è una nuova chat creata automaticamente, `false` se è una chat esistente + +### 2. Status Event +Informazioni sullo stato di elaborazione. + +``` +data: {"type": "status", "message": "Processing with MCP tools..."} +``` + +### 3. Tool Call Event +Notifica quando viene chiamato un tool MCP. + +``` +data: {"type": "tool_call", "tool_name": "create_task", "tool_args": {...}, "item_index": 0} +``` + +**Campi:** +- `type`: "tool_call" +- `tool_name`: Nome del tool chiamato +- `tool_args`: Argomenti passati al tool +- `item_index`: Indice dell'item nel contesto di streaming + +### 4. Tool Output Event +Risultato dell'esecuzione di un tool. + +``` +data: {"type": "tool_output", "tool_name": "create_task", "output": "...", "item_index": 0} +``` + +**Campi:** +- `type`: "tool_output" +- `tool_name`: Nome del tool +- `output`: Output del tool (JSON stringificato) +- `item_index`: Indice dell'item per correlare con tool_call + +### 5. Content Event +Chunk di testo della risposta dell'assistente. + +``` +data: {"type": "content", "delta": "Ho creato"} +data: {"type": "content", "delta": " il task"} +data: {"type": "content", "delta": " con successo!"} +``` + +**Campi:** +- `type`: "content" +- `delta`: Frammento di testo da aggiungere alla risposta + +### 6. Error Event +Notifica di errore durante l'elaborazione. + +``` +data: {"type": "error", "message": "Errore durante l'elaborazione"} +``` + +### 7. Done Event +Indica il completamento dello stream. + +``` +data: {"type": "done", "message": "Stream completed"} +``` + +## Esempio Completo di Stream + +``` +data: {"type": "chat_info", "chat_id": "550e8400-e29b-41d4-a716-446655440000", "is_new": true} + +data: {"type": "status", "message": "Processing with MCP tools..."} + +data: {"type": "tool_call", "tool_name": "create_task", "tool_args": {"title": "Comprare il latte", "due_date": "2024-01-20"}, "item_index": 0} + +data: {"type": "tool_output", "tool_name": "create_task", "output": "{\"success\": true, \"task_id\": 123}", "item_index": 0} + +data: {"type": "content", "delta": "Ho creato"} + +data: {"type": "content", "delta": " il task"} + +data: {"type": "content", "delta": " 'Comprare il latte'"} + +data: {"type": "content", "delta": " con scadenza"} + +data: {"type": "content", "delta": " il 20 gennaio."} + +data: {"type": "done", "message": "Stream completed"} +``` + +## Gestione Client + +### TypeScript Client Implementation + +```typescript +import { sendMessageToBot } from './services/botservice'; + +// Stato per tracciare chat_id +const [currentChatId, setCurrentChatId] = useState(null); + +// Callback per gestire lo streaming +const onStreamChunk = ( + chunk: string, + isComplete: boolean, + toolWidgets?: ToolWidget[], + chatInfo?: { chat_id: string; is_new: boolean } +) => { + // Aggiorna chat_id se ricevuto dal server + if (chatInfo?.chat_id) { + setCurrentChatId(chatInfo.chat_id); + if (chatInfo.is_new) { + console.log('Nuova chat creata:', chatInfo.chat_id); + } + } + + // Gestisci chunk di testo + if (chunk) { + // Aggiungi chunk al messaggio + } + + // Gestisci tool widgets + if (toolWidgets) { + // Aggiorna UI con widgets + } + + // Se completato, finalizza il messaggio + if (isComplete) { + // Messaggio completato + } +}; + +// Invia messaggio +const result = await sendMessageToBot( + userMessage, + modelType, + onStreamChunk, + currentChatId || undefined +); + +// Aggiorna chat_id se cambiato +if (result.chat_id && result.chat_id !== currentChatId) { + setCurrentChatId(result.chat_id); +} +``` + +### Gestione Chat History + +```typescript +// Carica una chat esistente +import { getChatWithMessages } from './services/chatHistoryService'; + +const loadChat = async (chatId: string) => { + const chatData = await getChatWithMessages(chatId); + + // Imposta chat_id corrente + setCurrentChatId(chatId); + + // Carica messaggi nella UI + setMessages(chatData.messages.map(msg => ({ + id: msg.message_id.toString(), + text: msg.content, + sender: msg.role === 'user' ? 'user' : 'bot', + // ... altri campi + }))); +}; + +// Crea nuova chat +import { createNewChat } from './services/botservice'; + +const startNewChat = async () => { + const chatId = await createNewChat(); + setCurrentChatId(chatId); + setMessages([]); +}; +``` + +## Vantaggi del Nuovo Sistema + +1. **Riduzione Banda**: Non è più necessario inviare tutta la cronologia ad ogni messaggio +2. **Performance**: Meno dati da processare lato client e server +3. **Scalabilità**: La cronologia è gestita centralmente nel database +4. **Semplicità**: L'API client è più semplice e intuitiva +5. **Consistenza**: La cronologia è sempre sincronizzata con il server + +## Retrocompatibilità + +Il server supporta ancora richieste senza `chat_id`. In questo caso: +- Viene creata automaticamente una nuova chat +- Il `chat_id` viene restituito nell'evento `chat_info` +- Il client può salvare il `chat_id` per messaggi successivi + +## Best Practices + +1. **Persistenza Chat ID**: Salva sempre il `chat_id` ricevuto dal server +2. **Gestione Errori**: Implementa fallback se il `chat_id` non è più valido +3. **Nuova Chat**: Resetta il `chat_id` quando l'utente inizia una nuova conversazione +4. **Loading States**: Mostra indicatori di caricamento durante lo streaming +5. **Widget Management**: Usa `item_index` per correlare tool_call e tool_output + +## Troubleshooting + +### Chat ID non valido +Se il server non trova il `chat_id`, crea automaticamente una nuova chat e restituisce il nuovo ID nell'evento `chat_info`. + +### Tool Output con tool_name "unknown" +Il client usa `item_index` per correlare tool_output con tool_call, risolvendo il problema del tool_name "unknown". + +### Streaming interrotto +Gestisci l'evento `error` e mostra un messaggio appropriato all'utente. + +## Riferimenti + +- [Chat History API Documentation](./CHAT_HISTORY_API.md) +- [Bot Service Implementation](../src/services/botservice.ts) +- [Chat History Service](../src/services/chatHistoryService.ts) diff --git a/src/components/BotChat/ChatHistory.tsx b/src/components/BotChat/ChatHistory.tsx new file mode 100644 index 0000000..d17f21a --- /dev/null +++ b/src/components/BotChat/ChatHistory.tsx @@ -0,0 +1,368 @@ +import React, { useEffect, useState } from 'react'; +import { + View, + Text, + StyleSheet, + FlatList, + TouchableOpacity, + ActivityIndicator, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { ChatHistoryItem, ChatHistoryItemData } from './ChatHistoryItem'; +import { + fetchChatHistory, + deleteChatHistory, + ChatHistoryResponse, +} from '../../services/chatHistoryService'; + +interface ChatHistoryProps { + onChatPress: (chatId: string) => void; + onNewChat?: () => void; + onRefresh?: () => void; +} + +/** + * Transforms API response data to ChatHistoryItemData format + */ +const transformChatData = ( + apiChat: ChatHistoryResponse +): ChatHistoryItemData => ({ + id: apiChat.chat_id, + title: apiChat.title, + preview: apiChat.last_message_preview, + timestamp: new Date(apiChat.updated_at), + messageCount: apiChat.message_count, + isPinned: apiChat.is_pinned, +}); + +export const ChatHistory: React.FC = ({ + onChatPress, + onNewChat, + onRefresh, +}) => { + const [chats, setChats] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [skip, setSkip] = useState(0); + const LIMIT = 20; + const isLoadingRef = React.useRef(false); + + const loadChatHistory = async (reset: boolean = false) => { + // Prevent concurrent loads + if (isLoadingRef.current && !reset) { + console.log('[ChatHistory] Already loading, skipping...'); + return; + } + + try { + isLoadingRef.current = true; + + if (reset) { + setLoading(true); + setSkip(0); + } + setError(null); + + const currentSkip = reset ? 0 : skip; + const chatData = await fetchChatHistory({ + skip: currentSkip, + limit: LIMIT, + }); + + // Sort by pinned first, then by updated_at (API already returns sorted) + const sortedChats = chatData.map(transformChatData); + + if (reset) { + setChats(sortedChats); + } else { + setChats((prevChats) => [...prevChats, ...sortedChats]); + } + + setHasMore(chatData.length === LIMIT); + setSkip(currentSkip + chatData.length); + } catch (err) { + console.error('Failed to load chat history:', err); + setError('Failed to load chat history'); + } finally { + setLoading(false); + setLoadingMore(false); + setRefreshing(false); + isLoadingRef.current = false; + } + }; + + const loadMoreChats = async () => { + if (loadingMore || !hasMore) return; + + setLoadingMore(true); + await loadChatHistory(false); + }; + + const handlePullRefresh = async () => { + setRefreshing(true); + isLoadingRef.current = false; // Reset flag to allow refresh + await loadChatHistory(true); + if (onRefresh) { + onRefresh(); + } + }; + + const handleChatDelete = async (chatId: string) => { + try { + await deleteChatHistory(chatId); + setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId)); + } catch (err) { + console.error('Failed to delete chat:', err); + setError('Failed to delete chat'); + } + }; + + const handleTogglePin = async (chatId: string, isPinned: boolean) => { + try { + const { toggleChatPin } = await import('../../services/chatHistoryService'); + const updatedChat = await toggleChatPin(chatId, isPinned); + + setChats((prevChats) => + prevChats.map((chat) => + chat.id === chatId + ? { + ...chat, + isPinned: updatedChat.is_pinned, + } + : chat + ) + ); + + // Re-sort after pin change + setChats((prevChats) => + [...prevChats].sort((a, b) => { + if (a.isPinned && !b.isPinned) return -1; + if (!a.isPinned && b.isPinned) return 1; + return b.timestamp.getTime() - a.timestamp.getTime(); + }) + ); + } catch (err) { + console.error('Failed to toggle pin:', err); + setError('Failed to update chat'); + } + }; + + const handleRefresh = () => { + isLoadingRef.current = false; // Reset flag to allow refresh + loadChatHistory(true); + if (onRefresh) { + onRefresh(); + } + }; + + useEffect(() => { + // Load chat history only once on mount + loadChatHistory(true); + }, []); + const renderHeader = () => ( + + + Chat History + + {chats.length} + + + + + + + {onNewChat && ( + + + + )} + + + ); + + const renderEmptyState = () => { + if (loading) { + return ( + + + Loading chat history... + + ); + } + + if (error) { + return ( + + + Error + {error} + + Retry + + + ); + } + + return ( + + + No chat history + + Start a conversation to see it here + + + ); + }; + + const renderItem = ({ item }: { item: ChatHistoryItemData }) => ( + + ); + + const renderFooter = () => { + if (!loadingMore) return null; + + return ( + + + + ); + }; + + return ( + + {renderHeader()} + item.id} + contentContainerStyle={styles.listContent} + ListEmptyComponent={renderEmptyState} + ListFooterComponent={renderFooter} + showsVerticalScrollIndicator={false} + refreshing={refreshing} + onRefresh={handlePullRefresh} + onEndReached={loadMoreChats} + onEndReachedThreshold={0.5} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#ffffff', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: '#e1e5e9', + }, + headerLeft: { + flexDirection: 'row', + alignItems: 'center', + }, + headerRight: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + headerTitle: { + fontSize: 20, + fontWeight: '600', + color: '#000000', + fontFamily: 'System', + marginRight: 8, + }, + countBadge: { + backgroundColor: '#f0f0f0', + borderRadius: 12, + paddingHorizontal: 10, + paddingVertical: 4, + minWidth: 32, + alignItems: 'center', + }, + countText: { + fontSize: 13, + color: '#666666', + fontWeight: '600', + fontFamily: 'System', + }, + refreshButton: { + padding: 4, + }, + newChatButton: { + padding: 4, + }, + listContent: { + paddingVertical: 8, + flexGrow: 1, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 40, + paddingTop: 60, + }, + emptyTitle: { + fontSize: 18, + fontWeight: '600', + color: '#999999', + marginTop: 16, + fontFamily: 'System', + }, + emptySubtitle: { + fontSize: 14, + color: '#cccccc', + marginTop: 8, + textAlign: 'center', + fontFamily: 'System', + }, + retryButton: { + marginTop: 16, + backgroundColor: '#007AFF', + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + retryButtonText: { + color: '#ffffff', + fontSize: 16, + fontWeight: '600', + fontFamily: 'System', + }, + footerLoader: { + paddingVertical: 20, + alignItems: 'center', + }, +}); diff --git a/src/components/BotChat/ChatHistoryItem.tsx b/src/components/BotChat/ChatHistoryItem.tsx new file mode 100644 index 0000000..0dadbdb --- /dev/null +++ b/src/components/BotChat/ChatHistoryItem.tsx @@ -0,0 +1,213 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; + +export interface ChatHistoryItemData { + id: string; + title: string; + preview: string; + timestamp: Date; + messageCount: number; + isPinned?: boolean; +} + +interface ChatHistoryItemProps { + chat: ChatHistoryItemData; + onPress: (chatId: string) => void; + onDelete?: (chatId: string) => void; + onTogglePin?: (chatId: string, isPinned: boolean) => void; +} + +export const ChatHistoryItem: React.FC = ({ + chat, + onPress, + onDelete, + onTogglePin, +}) => { + const formatTimestamp = (date: Date) => { + const now = new Date(); + // Add 240 minutes (4 hours) to compensate for timezone offset + const diffInMs = now.getTime() - date.getTime() + (240 * 60 * 1000); + const diffInHours = diffInMs / (1000 * 60 * 60); + const diffInDays = diffInMs / (1000 * 60 * 60 * 24); + + if (diffInHours < 1) { + const minutes = Math.floor(diffInMs / (1000 * 60)); + return `${minutes}m ago`; + } else if (diffInHours < 24) { + return `${Math.floor(diffInHours)}h ago`; + } else if (diffInDays < 7) { + return `${Math.floor(diffInDays)}d ago`; + } else { + return date.toLocaleDateString('it-IT', { day: 'numeric', month: 'short' }); + } + }; + + return ( + onPress(chat.id)} + activeOpacity={0.7} + > + + + + + + + + {chat.isPinned && ( + + )} + + {chat.title} + + + + {formatTimestamp(chat.timestamp)} + + + + + + {chat.preview} + + + {chat.messageCount} + + + + + + {onTogglePin && ( + onTogglePin(chat.id, !chat.isPinned)} + activeOpacity={0.7} + > + + + )} + {onDelete && ( + onDelete(chat.id)} + activeOpacity={0.7} + > + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#ffffff', + borderRadius: 12, + padding: 12, + marginHorizontal: 16, + marginVertical: 6, + borderWidth: 1, + borderColor: '#e1e5e9', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#f8f9fa', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + contentContainer: { + flex: 1, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + titleRow: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + pinIcon: { + marginRight: 4, + }, + title: { + flex: 1, + fontSize: 16, + fontWeight: '600', + color: '#000000', + fontFamily: 'System', + marginRight: 8, + }, + timestamp: { + fontSize: 12, + color: '#999999', + fontFamily: 'System', + }, + previewRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + preview: { + flex: 1, + fontSize: 14, + color: '#666666', + fontFamily: 'System', + marginRight: 8, + }, + messageCountBadge: { + backgroundColor: '#f0f0f0', + borderRadius: 10, + paddingHorizontal: 8, + paddingVertical: 2, + minWidth: 24, + alignItems: 'center', + }, + messageCountText: { + fontSize: 11, + color: '#666666', + fontWeight: '600', + fontFamily: 'System', + }, + actionsContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + marginLeft: 8, + }, + actionButton: { + padding: 8, + }, +}); diff --git a/src/data/demoChatHistory.ts b/src/data/demoChatHistory.ts new file mode 100644 index 0000000..79cac63 --- /dev/null +++ b/src/data/demoChatHistory.ts @@ -0,0 +1,63 @@ +import { ChatHistoryItemData } from '../components/BotChat/ChatHistoryItem'; + +/** + * Demo chat history data for testing and development + */ +export const demoChatHistory: ChatHistoryItemData[] = [ + { + id: 'chat-1', + title: 'Planning my week', + preview: 'Can you help me organize my tasks for this week?', + timestamp: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago + messageCount: 12, + }, + { + id: 'chat-2', + title: 'Project brainstorming', + preview: 'I need ideas for the new mobile app design', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 3), // 3 hours ago + messageCount: 8, + }, + { + id: 'chat-3', + title: 'Task priority discussion', + preview: 'Which tasks should I focus on today?', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago + messageCount: 15, + }, + { + id: 'chat-4', + title: 'Meeting preparation', + preview: 'Help me prepare for tomorrow\'s team meeting', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago + messageCount: 6, + }, + { + id: 'chat-5', + title: 'Goal setting for Q2', + preview: 'Let\'s define my quarterly objectives', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), // 3 days ago + messageCount: 20, + }, + { + id: 'chat-6', + title: 'Weekend plans', + preview: 'What activities should I schedule for the weekend?', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5), // 5 days ago + messageCount: 4, + }, + { + id: 'chat-7', + title: 'Learning roadmap', + preview: 'Create a learning plan for React Native', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), // 1 week ago + messageCount: 18, + }, + { + id: 'chat-8', + title: 'Budget planning', + preview: 'Help me organize my monthly expenses', + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 10), // 10 days ago + messageCount: 9, + }, +]; diff --git a/src/navigation/screens/BotChat.tsx b/src/navigation/screens/BotChat.tsx index 217c65a..7d66a33 100644 --- a/src/navigation/screens/BotChat.tsx +++ b/src/navigation/screens/BotChat.tsx @@ -1,12 +1,13 @@ import React, { useState, useCallback, useEffect } from 'react'; import { View, KeyboardAvoidingView, Platform, SafeAreaView, Alert, Keyboard, Dimensions } from 'react-native'; import { sendMessageToBot, createNewChat, formatMessage, clearChatHistory } from '../../services/botservice'; -import { - ChatHeader, - ChatInput, - ChatList, +import { getChatWithMessages, ChatMessage } from '../../services/chatHistoryService'; +import { + ChatHeader, + ChatInput, + ChatList, Message, - chatStyles + chatStyles } from '../../components/BotChat'; const BotChat: React.FC = () => { @@ -14,6 +15,7 @@ const BotChat: React.FC = () => { const [messages, setMessages] = useState([]); const [modelType, setModelType] = useState<'base' | 'advanced'>('base'); const [keyboardVisible, setKeyboardVisible] = useState(false); + const [currentChatId, setCurrentChatId] = useState(null); // Costanti const USER = 'user'; @@ -82,53 +84,124 @@ const BotChat: React.FC = () => { keyboardDidHideListener?.remove(); }; }, [deviceType]); - // Funzione per inizializzare la chat con messaggi di benvenuto + // Funzione per inizializzare la chat creando una nuova sessione sul server const initializeChat = async () => { - const welcomeMessages = await createNewChat(); - setMessages(welcomeMessages as Message[]); + try { + // Crea una nuova sessione chat sul server + const chatId = await createNewChat(); + setCurrentChatId(chatId); + setMessages([]); + console.log('✅ Nuova chat inizializzata con ID:', chatId); + } catch (error) { + console.error('❌ Errore durante l\'inizializzazione della chat:', error); + // In caso di errore, continua comunque senza chat_id (modalità offline) + setCurrentChatId(null); + setMessages([]); + } + }; + + // Funzione per caricare una chat esistente dal server + const loadExistingChat = async (chatId: string) => { + try { + console.log('📥 Caricamento chat esistente:', chatId); + + // Recupera la chat con tutti i suoi messaggi dal server + const chatData = await getChatWithMessages(chatId); + + // Trasforma i messaggi dall'API al formato UI + const transformedMessages: Message[] = chatData.messages.map((apiMsg: ChatMessage) => ({ + id: apiMsg.message_id.toString(), + text: apiMsg.content, + sender: apiMsg.role === 'user' ? USER : BOT, + start_time: new Date(apiMsg.created_at), + modelType: apiMsg.model as 'base' | 'advanced' | undefined, + isStreaming: false, + isComplete: true, + })); + + setCurrentChatId(chatId); + setMessages(transformedMessages); + + console.log('✅ Chat caricata con successo:', { + chatId, + title: chatData.title, + messageCount: transformedMessages.length + }); + } catch (error) { + console.error('❌ Errore durante il caricamento della chat:', error); + Alert.alert( + 'Errore', + 'Impossibile caricare la chat. Riprova più tardi.', + [{ text: 'OK' }] + ); + } }; // Handler per creare una nuova chat const handleNewChat = () => { - Alert.alert( - "Nuova Chat", - "Vuoi creare una nuova chat? Tutti i messaggi attuali verranno eliminati sia localmente che dal server.", - [ - { - text: "Annulla", - style: "cancel" - }, - { - text: "Conferma", - onPress: async () => { - try { - // Elimina la cronologia dal server - const serverCleared = await clearChatHistory(); - - if (!serverCleared) { - // Mostra un avviso ma procedi comunque con la pulizia locale + // Se c'è una chat aperta, semplicemente pulisci e esci dalla sessione + if (currentChatId) { + Alert.alert( + "Pulisci Chat", + "Vuoi pulire la chat corrente? I messaggi verranno rimossi localmente ma la cronologia sul server rimarrà intatta.", + [ + { + text: "Annulla", + style: "cancel" + }, + { + text: "Conferma", + onPress: () => { + // Pulisci solo localmente senza creare una nuova sessione + setMessages([]); + setCurrentChatId(null); + console.log('✅ Chat pulita e uscito dalla sessione'); + } + } + ] + ); + } else { + // Se non c'è una chat aperta, crea una nuova sessione + Alert.alert( + "Nuova Chat", + "Vuoi creare una nuova chat? Tutti i messaggi attuali verranno eliminati sia localmente che dal server.", + [ + { + text: "Annulla", + style: "cancel" + }, + { + text: "Conferma", + onPress: async () => { + try { + // Elimina la cronologia dal server + const serverCleared = await clearChatHistory(); + + if (!serverCleared) { + // Mostra un avviso ma procedi comunque con la pulizia locale + Alert.alert( + "Avviso", + "Non è stato possibile eliminare la cronologia dal server, ma la chat locale verrà comunque resettata.", + [{ text: "OK", onPress: () => initializeChat() }] + ); + } else { + // Tutto ok, procedi con la pulizia locale + await initializeChat(); + } + } catch (error) { + console.error("Errore durante il reset della chat:", error); + // In caso di errore, procedi comunque con la pulizia locale Alert.alert( - "Avviso", - "Non è stato possibile eliminare la cronologia dal server, ma la chat locale verrà comunque resettata.", + "Errore", + "Si è verificato un errore durante l'eliminazione della cronologia dal server, ma la chat locale verrà resettata.", [{ text: "OK", onPress: () => initializeChat() }] ); - } else { - // Tutto ok, procedi con la pulizia locale - await initializeChat(); } - } catch (error) { - console.error("Errore durante il reset della chat:", error); - // In caso di errore, procedi comunque con la pulizia locale - Alert.alert( - "Errore", - "Si è verificato un errore durante l'eliminazione della cronologia dal server, ma la chat locale verrà resettata.", - [{ text: "OK", onPress: () => initializeChat() }] - ); } } - } - ] - ); + ] + ); + } }; // Handler per cambiare il tipo di modello @@ -185,9 +258,15 @@ const BotChat: React.FC = () => { // 3. Accumulo dati streaming let accumulatedText = ""; let currentWidgets: any[] = []; + let receivedChatId: string | undefined; // 4. Callback per aggiornare UI durante streaming - const onStreamChunk = (chunk: string, isComplete: boolean, toolWidgets?: any[]) => { + const onStreamChunk = ( + chunk: string, + isComplete: boolean, + toolWidgets?: any[], + chatInfo?: { chat_id: string; is_new: boolean } + ) => { if (chunk) { accumulatedText += chunk; } @@ -196,6 +275,14 @@ const BotChat: React.FC = () => { currentWidgets = toolWidgets; } + // Se riceviamo chat_id dal server, aggiorniamo lo stato + if (chatInfo?.chat_id) { + receivedChatId = chatInfo.chat_id; + if (chatInfo.is_new) { + console.log('[BotChat] Nuova chat creata automaticamente dal server:', receivedChatId); + } + } + // Aggiorna il messaggio bot con testo + widgets accumulati setMessages(prevMessages => prevMessages.map(msg => @@ -213,18 +300,20 @@ const BotChat: React.FC = () => { }; try { - // 5. Otteniamo gli ultimi messaggi per contesto - const currentMessages = [...messages, userMessage]; - const lastMessages = currentMessages.slice(-6); // Ultimi 6 messaggi - - // 6. Invia richiesta con streaming callback + // 5. Invia richiesta con streaming callback e chat_id const result = await sendMessageToBot( text, modelType, - lastMessages, - onStreamChunk + onStreamChunk, + currentChatId || undefined ); + // 6. Aggiorna currentChatId se il server ha restituito un chat_id + if (result.chat_id && result.chat_id !== currentChatId) { + console.log('[BotChat] Aggiornamento chat_id da:', currentChatId, 'a:', result.chat_id); + setCurrentChatId(result.chat_id); + } + // 7. Aggiornamento finale con dati completi const formattedText = formatMessage(result.text); @@ -261,7 +350,7 @@ const BotChat: React.FC = () => { ) ); } - }, [modelType, messages]); + }, [modelType, messages, currentChatId]); const keyboardConfig = getKeyboardAvoidingViewConfig(); // Per iPad, usiamo un approccio ibrido: KeyboardAvoidingView con offset minimo diff --git a/src/navigation/screens/Home.tsx b/src/navigation/screens/Home.tsx index 995165d..cb7e415 100644 --- a/src/navigation/screens/Home.tsx +++ b/src/navigation/screens/Home.tsx @@ -14,12 +14,14 @@ import { Platform, Alert, } from "react-native"; -import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scrollview"; +import { GestureDetector, Gesture, GestureHandlerRootView } from "react-native-gesture-handler"; +import { runOnJS } from "react-native-reanimated"; import { Ionicons } from "@expo/vector-icons"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { ChatList, Message } from "../../components/BotChat"; import { ToolWidget } from "../../components/BotChat/types"; -import { sendMessageToBot, formatMessage, clearChatHistory, StreamingCallback } from "../../services/botservice"; +import { sendMessageToBot, formatMessage, StreamingCallback, createNewChat } from "../../services/botservice"; +import { getChatWithMessages, ChatMessage } from "../../services/chatHistoryService"; import { STORAGE_KEYS } from "../../constants/authConstants"; import { TaskCacheService } from '../../services/TaskCacheService'; import SyncManager, { SyncStatus } from '../../services/SyncManager'; @@ -27,6 +29,7 @@ import Badge from "../../components/UI/Badge"; import VoiceChatModal from "../../components/BotChat/VoiceChatModal"; import { useTutorialContext } from "../../contexts/TutorialContext"; import { useTranslation } from 'react-i18next'; +import { ChatHistory } from "../../components/BotChat/ChatHistory"; const HomeScreen = () => { const { t } = useTranslation(); @@ -40,12 +43,17 @@ const HomeScreen = () => { const [syncStatus, setSyncStatus] = useState(null); const [suggestedCommandUsed, setSuggestedCommandUsed] = useState(false); const [screenHeight, setScreenHeight] = useState(Dimensions.get('window').height); - const [keyboardHeight, setKeyboardHeight] = useState(0); const [isInputFocused, setIsInputFocused] = useState(false); + const [showChatHistory, setShowChatHistory] = useState(false); + const [currentChatId, setCurrentChatId] = useState(null); // Tutorial context const tutorialContext = useTutorialContext(); + // Costanti + const USER = 'user'; + const BOT = 'bot'; + const handleStartTutorial = () => { console.log('[HOME] Tutorial context:', tutorialContext); console.log('[HOME] startTutorial function:', tutorialContext?.startTutorial); @@ -69,6 +77,8 @@ const HomeScreen = () => { const inputBottomPosition = useRef(new Animated.Value(0)).current; const cursorOpacity = useRef(new Animated.Value(1)).current; const micButtonAnim = useRef(new Animated.Value(1)).current; + const chatHistorySlideIn = useRef(new Animated.Value(0)).current; + const chatHistoryOpacity = useRef(new Animated.Value(0)).current; // Setup sync status listener useEffect(() => { const handleSyncStatus = (status: SyncStatus) => { @@ -140,7 +150,7 @@ const HomeScreen = () => { clearInterval(typingInterval); }; } - }, [userName, chatStarted]); + }, [userName, chatStarted, t]); // Effetto per l'animazione del cursore lampeggiante useEffect(() => { @@ -192,7 +202,6 @@ const HomeScreen = () => { const keyboardDidShowListener = Keyboard.addListener( "keyboardDidShow", (event) => { - setKeyboardHeight(event.endCoordinates.height); if (chatStarted) { // Sposta l'input sopra la tastiera con margine maggiore Animated.timing(inputBottomPosition, { @@ -207,7 +216,6 @@ const HomeScreen = () => { const keyboardDidHideListener = Keyboard.addListener( "keyboardDidHide", () => { - setKeyboardHeight(0); if (chatStarted) { // Riporta l'input in posizione normale Animated.timing(inputBottomPosition, { @@ -268,6 +276,19 @@ const HomeScreen = () => { const trimmedMessage = message.trim(); if (!trimmedMessage || isLoading) return; + // Se non c'è un chat_id corrente, crea una nuova sessione + let chatIdToUse = currentChatId; + if (!chatIdToUse) { + try { + chatIdToUse = await createNewChat(); + setCurrentChatId(chatIdToUse); + console.log('✅ Nuova chat creata automaticamente con ID:', chatIdToUse); + } catch (error) { + console.error('❌ Errore durante la creazione automatica della chat:', error); + // Continua senza chat_id (modalità offline) + } + } + const userMessage: Message = { id: generateMessageId(), text: trimmedMessage, @@ -316,7 +337,12 @@ const HomeScreen = () => { try { // Callback per gestire lo streaming - const onStreamChunk: StreamingCallback = (chunk: string, isComplete: boolean, toolWidgets?: ToolWidget[]) => { + const onStreamChunk: StreamingCallback = ( + chunk: string, + isComplete: boolean, + toolWidgets?: ToolWidget[], + chatInfo?: { chat_id: string; is_new: boolean } + ) => { if (typeof chunk !== 'string' && chunk) { console.warn('[HOME] onStreamChunk received non-string chunk:', chunk); } @@ -324,9 +350,18 @@ const HomeScreen = () => { isComplete, chunkPreview: typeof chunk === 'string' ? chunk.slice(0, 40) : chunk, widgetsCount: toolWidgets?.length || 0, - widgets: toolWidgets?.map(w => ({ toolName: w.toolName, status: w.status, type: w.toolOutput?.type })) + widgets: toolWidgets?.map(w => ({ toolName: w.toolName, status: w.status, type: w.toolOutput?.type })), + chatInfo }); + // Se riceviamo chat_id dal server, aggiorniamo lo stato + if (chatInfo?.chat_id) { + if (chatInfo.is_new) { + console.log('[HOME] Nuova chat creata automaticamente dal server:', chatInfo.chat_id); + } + setCurrentChatId(chatInfo.chat_id); + } + if (isComplete) { // Lo streaming è completato, applica formatMessage al testo completo e aggiorna toolWidgets setMessages((prev) => @@ -358,14 +393,20 @@ const HomeScreen = () => { } }; - // Invia il messaggio al bot con streaming - await sendMessageToBot( + // Invia il messaggio al bot con streaming e chat_id + const result = await sendMessageToBot( trimmedMessage, "advanced", - messages, - onStreamChunk + onStreamChunk, + chatIdToUse || undefined ); + // Aggiorna currentChatId se il server ha restituito un chat_id + if (result.chat_id && result.chat_id !== currentChatId) { + console.log('[HOME] Aggiornamento chat_id da:', currentChatId, 'a:', result.chat_id); + setCurrentChatId(result.chat_id); + } + } catch (error) { console.error("[HOME] Errore nell'invio del messaggio:", error); @@ -387,12 +428,23 @@ const HomeScreen = () => { }; const handleResetChat = async () => { - try { - // Prima elimina la cronologia dal server - await clearChatHistory(); - } catch (error) { - console.error("Errore durante l'eliminazione della cronologia dal server:", error); - // Procedi comunque con la pulizia locale anche se fallisce quella del server + // Se c'è una chat aperta, semplicemente pulisci e esci dalla sessione + if (currentChatId) { + console.log('[HOME] Clearing current chat session:', currentChatId); + setCurrentChatId(null); + } else { + console.log('[HOME] Starting new chat (reset current chat)'); + + try { + // Crea una nuova sessione chat sul server solo se non c'è una chat aperta + const chatId = await createNewChat(); + setCurrentChatId(chatId); + console.log('✅ Nuova chat creata con ID:', chatId); + } catch (error) { + console.error('❌ Errore durante la creazione della nuova chat:', error); + // Continua comunque con il reset locale anche se fallisce la creazione sul server + setCurrentChatId(null); + } } // Animazione di uscita per i messaggi @@ -460,6 +512,120 @@ const HomeScreen = () => { setMessages((prev) => [...prev, botMessage]); }; + const handleChatHistoryPress = async (chatId: string) => { + console.log('[HOME] Opening chat history:', chatId); + + try { + // Recupera la chat con tutti i suoi messaggi dal server + const chatData = await getChatWithMessages(chatId); + + // Trasforma i messaggi dall'API al formato UI + const transformedMessages: Message[] = chatData.messages.map((apiMsg: ChatMessage) => ({ + id: apiMsg.message_id.toString(), + text: apiMsg.content, + sender: apiMsg.role === 'user' ? USER : BOT, + start_time: new Date(apiMsg.created_at), + modelType: apiMsg.model as 'base' | 'advanced' | undefined, + isStreaming: false, + isComplete: true, + })); + + // Imposta la chat corrente + setCurrentChatId(chatId); + setMessages(transformedMessages); + + // Avvia la modalità chat se non è già avviata + if (!chatStarted) { + startChatAnimation(); + } + + // Chiudi la cronologia + setShowChatHistory(false); + + console.log('✅ Chat caricata con successo:', { + chatId, + title: chatData.title, + messageCount: transformedMessages.length + }); + } catch (error) { + console.error('❌ Errore durante il caricamento della chat:', error); + Alert.alert( + 'Errore', + 'Impossibile caricare la chat. Riprova più tardi.', + [{ text: 'OK' }] + ); + } + }; + + const handleNewChat = async () => { + // Se c'è una chat aperta, semplicemente pulisci e esci dalla sessione + if (currentChatId) { + console.log('[HOME] Clearing current chat session:', currentChatId); + setCurrentChatId(null); + setMessages([]); + setChatStarted(false); + setShowChatHistory(false); + console.log('✅ Chat pulita e uscito dalla sessione'); + return; + } + + // Altrimenti, crea una nuova sessione chat sul server + console.log('[HOME] Starting new chat from history'); + + try { + const chatId = await createNewChat(); + setCurrentChatId(chatId); + setMessages([]); + setChatStarted(false); + setShowChatHistory(false); + + console.log('✅ Nuova chat creata con ID:', chatId); + } catch (error) { + console.error('❌ Errore durante la creazione della nuova chat:', error); + // In caso di errore, continua comunque senza chat_id + setCurrentChatId(null); + setMessages([]); + setChatStarted(false); + setShowChatHistory(false); + } + }; + + const handleToggleChatHistory = () => { + if (!showChatHistory) { + // Apri la cronologia con animazione + setShowChatHistory(true); + Animated.parallel([ + Animated.timing(chatHistoryOpacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.spring(chatHistorySlideIn, { + toValue: 1, + tension: 50, + friction: 8, + useNativeDriver: true, + }), + ]).start(); + } else { + // Chiudi la cronologia con animazione + Animated.parallel([ + Animated.timing(chatHistoryOpacity, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(chatHistorySlideIn, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(() => { + setShowChatHistory(false); + }); + } + }; + // Calcolo dinamico del padding top basato sull'altezza dello schermo const getGreetingPaddingTop = () => { if (screenHeight < 700) return Math.max(screenHeight * 0.15, 80); // Schermi piccoli @@ -467,9 +633,34 @@ const HomeScreen = () => { return Math.max(screenHeight * 0.25, 180); // Schermi grandi }; + // Gesto swipe da destra a sinistra per aprire la cronologia + const swipeGesture = Gesture.Pan() + .activeOffsetX([-10, 10]) // Inizia il gesto con almeno 10px di movimento orizzontale + .failOffsetY([-20, 20]) // Fallisce se c'è troppo movimento verticale + .onEnd((event) => { + 'worklet'; + console.log('[HOME] Swipe gesture detected:', { + translationX: event.translationX, + velocityX: event.velocityX, + showChatHistory, + }); + + // Swipe da destra a sinistra per aprire (soglia più bassa per il simulatore) + if (event.translationX < -50 && !showChatHistory) { + console.log('[HOME] Opening chat history via swipe'); + runOnJS(handleToggleChatHistory)(); + } + // Swipe da sinistra a destra per chiudere + else if (event.translationX > 50 && showChatHistory) { + console.log('[HOME] Closing chat history via swipe'); + runOnJS(handleToggleChatHistory)(); + } + }); + return ( - - + + + {/* Header con titolo principale e indicatori sync */} @@ -511,24 +702,69 @@ const HomeScreen = () => { - {chatStarted && ( + {/* Chat History Toggle Button */} + + + + + {chatStarted && !showChatHistory && ( - + )} - - {/* Contenuto principale */} - - {/* Saluto personalizzato - nascosto quando la chat inizia */} - {!chatStarted && ( + + + {/* Chat History View */} + {showChatHistory ? ( + + + + ) : ( + <> + {/* Contenuto principale */} + + {/* Saluto personalizzato - nascosto quando la chat inizia */} + {!chatStarted && ( @@ -710,7 +946,10 @@ const HomeScreen = () => { )} - + + )} + + {/* Voice Chat Modal */} { isRecording={isRecording} onVoiceResponse={handleVoiceResponse} /> - + + ); }; @@ -1016,6 +1256,10 @@ const styles = StyleSheet.create({ suggestedCommandTextDisabled: { color: "#999999", }, + chatHistoryContainer: { + flex: 1, + backgroundColor: "#ffffff", + }, }); export default HomeScreen; diff --git a/src/services/botservice.ts b/src/services/botservice.ts index 4c1ac52..10947e8 100644 --- a/src/services/botservice.ts +++ b/src/services/botservice.ts @@ -4,12 +4,13 @@ import { ToolWidget } from '../components/BotChat/types'; /** - * Callback per gestire chunk di testo in streaming + widget tool + * Callback per gestire chunk di testo in streaming + widget tool + chat info */ export type StreamingCallback = ( chunk: string, isComplete: boolean, - toolWidgets?: ToolWidget[] + toolWidgets?: ToolWidget[], + chatInfo?: { chat_id: string; is_new: boolean } ) => void; /** @@ -17,16 +18,16 @@ export type StreamingCallback = ( * Utilizza l'endpoint /chat/text per la chat scritta con supporto streaming * @param {string} userMessage - Il messaggio dell'utente da inviare al bot * @param {string} modelType - Il tipo di modello da utilizzare ('base' o 'advanced') - * @param {Array} previousMessages - Gli ultimi messaggi scambiati tra utente e bot per il contesto * @param {StreamingCallback} onStreamChunk - Callback per ricevere chunk in streaming + widgets (opzionale) - * @returns {Promise<{text: string, toolWidgets: ToolWidget[]}>} - La risposta completa del bot con widgets + * @param {string} chatId - Optional chat ID to identify the chat session + * @returns {Promise<{text: string, toolWidgets: ToolWidget[], chat_id?: string, is_new?: boolean}>} - La risposta completa del bot con widgets e chat info */ export async function sendMessageToBot( userMessage: string, modelType: "base" | "advanced" = "base", - previousMessages: any[] = [], - onStreamChunk?: StreamingCallback -): Promise<{text: string, toolWidgets: ToolWidget[]}> { + onStreamChunk?: StreamingCallback, + chatId?: string +): Promise<{text: string, toolWidgets: ToolWidget[], chat_id?: string, is_new?: boolean}> { try { // Verifica che l'utente sia autenticato const token = await getValidToken(); @@ -35,11 +36,16 @@ export async function sendMessageToBot( } // Costruisci il payload per la richiesta - const requestPayload = { + const requestPayload: any = { quest: userMessage, model: modelType, }; + // Aggiungi chat_id se fornito per salvare i messaggi nella cronologia + if (chatId) { + requestPayload.chat_id = chatId; + } + // Invia la richiesta al server con supporto streaming usando expo fetch const response = await fetch("https://taskly-production.up.railway.app/chat/text", { method: "POST", @@ -69,6 +75,9 @@ export async function sendMessageToBot( const toolWidgetsMap = new Map(); // Mappa per tracciare tool_name per ogni item_index (workaround per tool_name: "unknown") const toolNamesMap = new Map(); + // Variabili per tracciare chat_id ricevuto dal server + let receivedChatId: string | undefined; + let isNewChat: boolean | undefined; try { while (true) { @@ -90,6 +99,21 @@ export async function sendMessageToBot( const jsonStr = line.replace('data: ', '').trim(); const parsed = JSON.parse(jsonStr); + // EVENTO: chat_info - Riceve informazioni sulla chat + if (parsed.type === 'chat_info') { + receivedChatId = parsed.chat_id; + isNewChat = parsed.is_new; + console.log(`[BOTSERVICE] Chat info ricevuto: chat_id=${receivedChatId}, is_new=${isNewChat}`); + + // Notifica UI del chat_id ricevuto + if (onStreamChunk) { + onStreamChunk('', false, Array.from(toolWidgetsMap.values()), { + chat_id: receivedChatId, + is_new: isNewChat || false, + }); + } + } + // EVENTO: tool_call - Crea widget in loading if (parsed.type === 'tool_call') { // Salva il tool_name per questo item_index @@ -228,12 +252,16 @@ export async function sendMessageToBot( // Notifica il completamento dello streaming if (onStreamChunk) { - onStreamChunk('', true, Array.from(toolWidgetsMap.values())); + onStreamChunk('', true, Array.from(toolWidgetsMap.values()), + receivedChatId ? { chat_id: receivedChatId, is_new: isNewChat || false } : undefined + ); } return { text: fullMessage || "Nessuna risposta ricevuta dal bot.", toolWidgets: Array.from(toolWidgetsMap.values()), + chat_id: receivedChatId, + is_new: isNewChat, }; } catch (error: any) { @@ -295,12 +323,24 @@ export async function clearChatHistory(): Promise { } /** - * Crea una nuova chat vuota - * @returns {Promise} - Array vuoto per inizializzare una nuova chat + * Crea una nuova sessione chat sul server + * @param {string} customChatId - Optional custom chat ID + * @returns {Promise} - Il chat_id della nuova sessione creata */ -export async function createNewChat(): Promise { - // Restituisce un array vuoto - i messaggi di benvenuto possono essere gestiti dall'UI - return []; +export async function createNewChat(customChatId?: string): Promise { + try { + // Importa la funzione createChat dal chatHistoryService + const { createChat } = await import('./chatHistoryService'); + + // Crea la sessione chat sul server + const chatData = await createChat(customChatId); + + console.log('✅ Nuova chat creata con ID:', chatData.chat_id); + return chatData.chat_id; + } catch (error) { + console.error('❌ Errore durante la creazione della chat:', error); + throw error; + } } /** diff --git a/src/services/chatHistoryService.ts b/src/services/chatHistoryService.ts new file mode 100644 index 0000000..15fd01a --- /dev/null +++ b/src/services/chatHistoryService.ts @@ -0,0 +1,235 @@ +import axios from './axiosInterceptor'; +import { getValidToken } from './authService'; + +export interface ChatHistoryResponse { + chat_id: string; + user_id: number; + title: string; + is_pinned: boolean; + created_at: string; + updated_at: string; + message_count: number; + last_message_preview: string | null; +} + +export interface ChatMessage { + message_id: number; + chat_id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + created_at: string; + token_count?: number; + model?: string; + tool_name?: string; + tool_input?: Record; + tool_output?: Record; +} + +export interface ChatWithMessages extends ChatHistoryResponse { + messages: ChatMessage[]; +} + +export interface ChatHistoryListResponse { + total: number; + chats: ChatHistoryResponse[]; +} + +export interface FetchChatHistoryOptions { + skip?: number; + limit?: number; + pinned_only?: boolean; +} + +/** + * Creates a new chat session + * @param customChatId - Optional custom chat ID + * @returns Promise with created chat data + */ +export const createChat = async ( + customChatId?: string +): Promise => { + try { + const token = await getValidToken(); + + if (!token) { + throw new Error('No valid authentication token'); + } + + const response = await axios.post( + '/chat/history/', + customChatId ? { chat_id: customChatId } : {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + return response.data; + } catch (error) { + console.error('Error creating chat:', error); + throw error; + } +}; + +/** + * Fetches the chat history list from the API + * @param options - Pagination and filter options + * @returns Promise with chat history data + */ +export const fetchChatHistory = async ( + options: FetchChatHistoryOptions = {} +): Promise => { + try { + const token = await getValidToken(); + + if (!token) { + throw new Error('No valid authentication token'); + } + + const { skip = 0, limit = 50, pinned_only = false } = options; + + const response = await axios.get( + '/chat/history/', + { + headers: { + Authorization: `Bearer ${token}`, + }, + params: { + skip, + limit, + pinned_only, + }, + } + ); + + return response.data.chats; + } catch (error) { + console.error('Error fetching chat history:', error); + throw error; + } +}; + +/** + * Fetches a specific chat with all its messages + * @param chatId - The ID of the chat to fetch + * @returns Promise with chat and messages + */ +export const getChatWithMessages = async ( + chatId: string +): Promise => { + try { + const token = await getValidToken(); + + if (!token) { + throw new Error('No valid authentication token'); + } + + const response = await axios.get( + `/chat/history/${chatId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + console.log('Fetched chat with messages:', response.data); + + return response.data; + } catch (error) { + console.error('Error fetching chat with messages:', error); + throw error; + } +}; + +/** + * Updates chat title + * @param chatId - The ID of the chat to update + * @param title - New title + * @returns Promise with updated chat data + */ +export const updateChatTitle = async ( + chatId: string, + title: string +): Promise => { + try { + const token = await getValidToken(); + + if (!token) { + throw new Error('No valid authentication token'); + } + + const response = await axios.patch( + `/chat/history/${chatId}`, + { title }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + return response.data; + } catch (error) { + console.error('Error updating chat title:', error); + throw error; + } +}; + +/** + * Toggles pin status of a chat + * @param chatId - The ID of the chat to toggle pin + * @param isPinned - New pin status + * @returns Promise with updated chat data + */ +export const toggleChatPin = async ( + chatId: string, + isPinned: boolean +): Promise => { + try { + const token = await getValidToken(); + + if (!token) { + throw new Error('No valid authentication token'); + } + + const response = await axios.patch( + `/chat/history/${chatId}`, + { is_pinned: isPinned }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + return response.data; + } catch (error) { + console.error('Error toggling chat pin:', error); + throw error; + } +}; + +/** + * Deletes a specific chat from history + * @param chatId - The ID of the chat to delete + */ +export const deleteChatHistory = async (chatId: string): Promise => { + try { + const token = await getValidToken(); + + if (!token) { + throw new Error('No valid authentication token'); + } + + await axios.delete(`/chat/history/${chatId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + } catch (error) { + console.error('Error deleting chat history:', error); + throw error; + } +}; diff --git a/test/test_botservice.ts b/test/test_botservice.ts index 98a38a4..3ef90dd 100644 --- a/test/test_botservice.ts +++ b/test/test_botservice.ts @@ -38,12 +38,15 @@ async function testSingleMessage(message: string, modelType: "base" | "advanced" // Invia il messaggio al bot const response = await sendMessageToBot(formattedMessage, modelType); - + const endTime = Date.now(); const responseTime = endTime - startTime; - + console.log(`📥 Risposta ricevuta (${responseTime}ms):`); - console.log(`"${response}"`); + console.log(`"${response.text}"`); + if (response.chat_id) { + console.log(`💬 Chat ID: ${response.chat_id} (new: ${response.is_new})`); + } console.log("✅ Test completato con successo"); } catch (error) { @@ -52,35 +55,31 @@ async function testSingleMessage(message: string, modelType: "base" | "advanced" } /** - * Esegue test con contesto (messaggi precedenti) + * Esegue test con chat_id (la cronologia è gestita dal server) */ -async function testWithContext(): Promise { - console.log("\n🧪 Test con contesto (messaggi precedenti)"); +async function testWithChatId(): Promise { + console.log("\n🧪 Test con chat_id (cronologia server-side)"); console.log("=".repeat(50)); - - const previousMessages = [ - { sender: "user", text: "Ciao" }, - { sender: "bot", text: "Ciao! Come posso aiutarti oggi?" }, - { sender: "user", text: "Sto pianificando la mia giornata" }, - { sender: "bot", text: "Perfetto! Posso aiutarti a organizzare le tue attività." } - ]; - + const newMessage = "Aiutami a creare una lista di 5 cose importanti da fare"; - + try { - console.log(`📚 Contesto: ${previousMessages.length} messaggi precedenti`); console.log(`📤 Nuovo messaggio: "${newMessage}"`); - + console.log(`💬 Note: La cronologia è ora gestita dal server tramite chat_id`); + const startTime = Date.now(); - const response = await sendMessageToBot(newMessage, "advanced", previousMessages); + const response = await sendMessageToBot(newMessage, "advanced"); const endTime = Date.now(); - - console.log(`📥 Risposta con contesto (${endTime - startTime}ms):`); - console.log(`"${response}"`); - console.log("✅ Test con contesto completato"); - + + console.log(`📥 Risposta (${endTime - startTime}ms):`); + console.log(`"${response.text}"`); + if (response.chat_id) { + console.log(`💬 Chat ID ricevuto: ${response.chat_id} (new: ${response.is_new})`); + } + console.log("✅ Test con chat_id completato"); + } catch (error) { - console.error("❌ Errore nel test con contesto:", error); + console.error("❌ Errore nel test con chat_id:", error); } } @@ -150,8 +149,8 @@ async function main(): Promise { await testSingleMessage("Ciao, come stai?", "base"); await testSingleMessage("Spiegami come organizzare le mie attività", "advanced"); - // Test con contesto - await testWithContext(); + // Test con chat_id + await testWithChatId(); // Test di performance (opzionale - commentalo se non vuoi sovraccaricare il server) // await testPerformance(); @@ -170,7 +169,7 @@ if (require.main === module) { export { testSingleMessage, - testWithContext, + testWithChatId, testPerformance, main };