From c1cbff0a7b921a2dabb41b8c5c7deea5a287c706 Mon Sep 17 00:00:00 2001 From: "Samuele E. Locatelli" Date: Wed, 3 Sep 2025 08:59:59 +0000 Subject: [PATCH] Fix timestap messaggi --- frontend/src/App.jsx.old | 81 --------- frontend/src/AssistantMessage.jsx | 52 +++--- frontend/src/AssistantMessage.jsx.orig | 32 ---- frontend/src/ChatLayout.jsx.orig | 79 --------- frontend/src/ChatWindow.jsx | 4 + frontend/src/UserMessage.jsx | 36 ++-- frontend/src/useChatStream.js | 232 +++++++++++++------------ 7 files changed, 175 insertions(+), 341 deletions(-) delete mode 100644 frontend/src/App.jsx.old delete mode 100644 frontend/src/AssistantMessage.jsx.orig delete mode 100644 frontend/src/ChatLayout.jsx.orig diff --git a/frontend/src/App.jsx.old b/frontend/src/App.jsx.old deleted file mode 100644 index 9541539..0000000 --- a/frontend/src/App.jsx.old +++ /dev/null @@ -1,81 +0,0 @@ -//App.jsx -import React, { useState, useEffect } from "react"; -import "./App.css"; -import { themes } from "./themes"; -import ChatLayout from "./ChatLayout"; -import { useChatStream } from "./useChatStream"; -import { getSessionId, getUserId, resetSessionId } from "./useSessionId"; - -export default function App() { - const { messages, loading, sendMessage, stopGenerating, setMessages } = useChatStream(); - const [themeName, setThemeName] = useState("light"); - const theme = themes[themeName]; - const sessionId = getSessionId(); - const userId = getUserId(); - - useEffect(() => { - const saved = localStorage.getItem("preferredTheme"); - if (saved && themes[saved]) setThemeName(saved); - }, []); - - useEffect(() => { - localStorage.setItem("preferredTheme", themeName); - }, [themeName]); - - const toggleTheme = () => { - setThemeName((t) => (t === "light" ? "dark" : "light")); - }; - - const reloadHistory = async () => { - const res = await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`); - const history = await res.json(); - setMessages(history); // from useChatStream - }; - - const freshStart = async () => { - await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`, { method: "DELETE" }); - setMessages([]); - resetSessionId(); - // or start a brand new sessionId: - //localStorage.removeItem("sessionId"); - //window.location.reload(); - }; - - const createSession = async () => { - const firstMessage = prompt("Enter a name or first message for the new session:") || ""; - const res = await fetch(`/v1/sessions?user_id=${userId}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ first_message: firstMessage }), - }); - const meta = await res.json(); - setSessionId(meta.session_id); - setMessages([]); // clear chat window - }; - - const editSession = async () => { - const newName = prompt("Enter a new name for this session:"); - if (!newName) return; - await fetch(`/v1/sessions/${sessionId}?user_id=${userId}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ session_name: newName }), - }); - }; - - return ( - - ); -} - - diff --git a/frontend/src/AssistantMessage.jsx b/frontend/src/AssistantMessage.jsx index 80a5aa2..1eed295 100644 --- a/frontend/src/AssistantMessage.jsx +++ b/frontend/src/AssistantMessage.jsx @@ -1,32 +1,45 @@ -// AssistantMessage.jsx +// src/AssistantMessage.jsx import React, { useState, useEffect } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; -export default function AssistantMessage({ content, theme, timestamp, isFinal }) { +export default function AssistantMessage({ + content, + theme, + timestamp, + startedAt, + endedAt, + isFinal +}) { const [showThink, setShowThink] = useState(false); const [fadeOut, setFadeOut] = useState(false); + // timestamp da mostrare: priorità a timestamp salvato, poi endedAt, poi startedAt + const ts = timestamp ?? endedAt ?? startedAt; + const thinkMatch = content?.match(/([\s\S]*?)<\/think>/i); const thinkContent = thinkMatch ? thinkMatch[1].trim() : null; - const visibleContent = isFinal + // Consideriamo "completo" se isFinal è true oppure se il messaggio ha un timestamp salvato + const isComplete = isFinal || Boolean(timestamp || endedAt); + + const visibleContent = isComplete ? content?.replace(/[\s\S]*?<\/think>/i, "").trim() : content; useEffect(() => { - if (thinkContent && !isFinal) { + if (thinkContent && !isComplete) { setShowThink(true); setFadeOut(false); } - if (thinkContent && isFinal) { + if (thinkContent && isComplete) { setFadeOut(true); const timer = setTimeout(() => setShowThink(false), 600); return () => clearTimeout(timer); } - }, [thinkContent, isFinal]); + }, [thinkContent, isComplete]); return (
@@ -62,7 +75,7 @@ export default function AssistantMessage({ content, theme, timestamp, isFinal }) {visibleContent}
- {timestamp && ( + {ts != null && (
- {formatDateTime(timestamp)} + {formatDateTime(ts)}
)} @@ -139,19 +152,16 @@ function CodeWithCopy({ inline, className = "", children, ...props }) { } function formatDateTime(dateTime) { - try { - const date = new Date(dateTime); - return date.toLocaleString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - }); - } catch { - return dateTime; - } + const date = dateTime instanceof Date ? dateTime : new Date(dateTime); + if (Number.isNaN(date.getTime())) return String(dateTime); + return date.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); } diff --git a/frontend/src/AssistantMessage.jsx.orig b/frontend/src/AssistantMessage.jsx.orig deleted file mode 100644 index 9070abc..0000000 --- a/frontend/src/AssistantMessage.jsx.orig +++ /dev/null @@ -1,32 +0,0 @@ -// src/AssistantMessage.jsx -import React from "react"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import remarkMath from "remark-math"; -import rehypeKatex from "rehype-katex"; - -export default function AssistantMessage({ content, theme }) { - return ( -
-
- ( - - ), - th: (props) =>
- }} - > - {content} - - - - ); -} - - diff --git a/frontend/src/ChatLayout.jsx.orig b/frontend/src/ChatLayout.jsx.orig deleted file mode 100644 index 2b29f0f..0000000 --- a/frontend/src/ChatLayout.jsx.orig +++ /dev/null @@ -1,79 +0,0 @@ -// src/ChatLayout.jsx -import React, { useState } from "react"; -import ChatHeader from "./ChatHeader"; -import ChatWindow from "./ChatWindow"; -import ChatInput from "./ChatInput"; -import SessionTable from "./SessionTable"; // your existing table - -export default function ChatLayout({ - theme, - messages, - loading, - onSend, - onStop, - onToggleTheme, - onReloadHistory, - onFreshStart, - onCreateSession, - onSelectSession, // new: load a chosen session - userId // new: so SessionTable can fetch sessions -}) { - const [showSessionsPanel, setShowSessionsPanel] = useState(false); - - const openSessionManager = () => setShowSessionsPanel(true); - const closeSessionManager = () => setShowSessionsPanel(false); - - return ( -
- - -
- -
- - {loading && ( -
- -
- )} - - - - {/* Offcanvas Session Manager */} -
-
-
Manage Sessions
- -
-
- { - onSelectSession(sessionId); // tell parent to load history - closeSessionManager(); - }} - /> -
-
-
- ); -} - - diff --git a/frontend/src/ChatWindow.jsx b/frontend/src/ChatWindow.jsx index c7e2210..cd84290 100644 --- a/frontend/src/ChatWindow.jsx +++ b/frontend/src/ChatWindow.jsx @@ -19,6 +19,8 @@ export default function ChatWindow({ messages, loading, theme }) { content={msg.content} theme={theme} timestamp={msg.timestamp} + startedAt={msg.startedAt} + endedAt={msg.endedAt} /> ) : ( ) diff --git a/frontend/src/UserMessage.jsx b/frontend/src/UserMessage.jsx index 28118cc..84e85fd 100644 --- a/frontend/src/UserMessage.jsx +++ b/frontend/src/UserMessage.jsx @@ -1,14 +1,17 @@ // src/UserMessage.jsx import React from "react"; -export default function UserMessage({ content, theme, timestamp }) { +export default function UserMessage({ content, theme, timestamp, startedAt, endedAt }) { + // Priorità: timestamp dal modello dati → endedAt → startedAt + const ts = timestamp ?? endedAt ?? startedAt; + return (
       
- {timestamp && ( + {ts != null && (
- {formatDateTime(timestamp)} + {formatDateTime(ts)}
)}
); } -// Same helper used in AssistantMessage function formatDateTime(dateTime) { - try { - const date = new Date(dateTime); - return date.toLocaleString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - }); - } catch { - return dateTime; - } + const date = dateTime instanceof Date ? dateTime : new Date(dateTime); + if (Number.isNaN(date.getTime())) return String(dateTime); + return date.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); } - diff --git a/frontend/src/useChatStream.js b/frontend/src/useChatStream.js index 285cd93..7b05531 100644 --- a/frontend/src/useChatStream.js +++ b/frontend/src/useChatStream.js @@ -1,4 +1,4 @@ -// useChatStream.js +// src/useChatStream.js import { useState, useCallback, useRef } from "react"; import { getSessionId, getUserId } from './useSessionId'; @@ -9,130 +9,144 @@ export function useChatStream() { const userId = getUserId(); - const sendMessage = useCallback( - async (input) => { - if (!input.trim()) return; + const sendMessage = useCallback(async (input) => { + if (!input.trim()) return; - const sessionId = getSessionId(); + const sessionId = getSessionId(); - if (abortRef.current) { - abortRef.current.abort(); - } - const controller = new AbortController(); - abortRef.current = controller; + // interrompe eventuale stream in corso + if (abortRef.current) { + abortRef.current.abort(); + } + const controller = new AbortController(); + abortRef.current = controller; - const userMessage = { role: "user", content: input }; - const assistantIndex = messages.length + 1; - setMessages((prev) => [ + const startedAt = Date.now(); + let assistantIndex; + + // aggiunge messaggi user + placeholder assistant + setMessages((prev) => { + assistantIndex = prev.length + 1; + return [ ...prev, - userMessage, - { role: "assistant", content: "", isFinal: false } - ]); - setLoading(true); + { role: "user", content: input, startedAt }, + { role: "assistant", content: "", isFinal: false, startedAt } + ]; + }); - try { - const res = await fetch("/v1/chat-stream", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify({ - user_id: userId, - session_id: sessionId, - message: input - }), - signal: controller.signal, - }); + setLoading(true); - if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`); + try { + const res = await fetch("/v1/chat-stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + user_id: userId, + session_id: sessionId, + message: input + }), + signal: controller.signal, + }); - const reader = res.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - let acc = ""; + if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`); - while (true) { - const { value, done } = await reader.read(); - if (done) break; + const reader = res.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let acc = ""; - buffer += decoder.decode(value, { stream: true }); - const parts = buffer.split("\n\n"); - buffer = parts.pop() || ""; + while (true) { + const { value, done } = await reader.read(); + if (done) break; - for (const part of parts) { - const line = part - .split("\n") - .map((l) => l.trim()) - .find((l) => l.startsWith("data:")); - if (!line) continue; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split("\n\n"); + buffer = parts.pop() || ""; - const data = line.slice(5).trim(); + for (const part of parts) { + const line = part + .split("\n") + .map((l) => l.trim()) + .find((l) => l.startsWith("data:")); + if (!line) continue; - if (data === "[DONE]") { - // Delay prima di settare isFinal: true - setTimeout(() => { - setMessages((prev) => { - const next = [...prev]; - if (next[assistantIndex]) { - next[assistantIndex] = { - ...next[assistantIndex], - content: acc, - isFinal: true - }; - } - return next; - }); - setLoading(false); - }, 800); // <-- delay in ms - return; - } - - try { - const obj = JSON.parse(data); - const piece = - obj?.choices?.[0]?.delta?.content ?? - obj?.choices?.[0]?.text ?? - ""; - if (piece) { - acc += piece; - setMessages((prev) => { - const next = [...prev]; - if (next[assistantIndex]) { - next[assistantIndex] = { - role: "assistant", - content: acc, - isFinal: false - }; - } - return next; - }); - } - } catch { - // ignora chunk non validi + const data = line.slice(5).trim(); + + if (data === "[DONE]") { + const endedAt = Date.now(); + // Delay prima di settare isFinal: true + setTimeout(() => { + setMessages((prev) => { + const next = [...prev]; + if (next[assistantIndex]) { + next[assistantIndex] = { + ...next[assistantIndex], + content: acc, + isFinal: true, + endedAt + }; + } + const userIdx = assistantIndex - 1; + if (userIdx >= 0 && next[userIdx]?.role === "user") { + next[userIdx] = { + ...next[userIdx], + endedAt + }; + } + return next; + }); + setLoading(false); + }, 800); // <-- delay in ms + return; + } + + try { + const obj = JSON.parse(data); + const piece = + obj?.choices?.[0]?.delta?.content ?? + obj?.choices?.[0]?.text ?? + ""; + if (piece) { + acc += piece; + setMessages((prev) => { + const next = [...prev]; + if (next[assistantIndex]) { + next[assistantIndex] = { + ...next[assistantIndex], // mantieni campi extra + content: acc, + isFinal: false + }; + } + return next; + }); } + } catch { + // ignora chunk non validi } } - } catch (err) { - if (err.name !== "AbortError") { - setMessages((prev) => { - const next = [...prev]; - if (next[assistantIndex]) { - next[assistantIndex] = { - role: "assistant", - content: `Error: ${String(err)}`, - isFinal: true - }; - } - return next; - }); - } - } finally { - abortRef.current = null; } - }, - [messages, userId] - ); + } catch (err) { + if (err.name !== "AbortError") { + setMessages((prev) => { + const next = [...prev]; + if (next[assistantIndex]) { + next[assistantIndex] = { + ...next[assistantIndex], + content: `Error: ${String(err)}`, + isFinal: true, + endedAt: Date.now() + }; + } + return next; + }); + } + } finally { + abortRef.current = null; + } + }, [userId]); const stopGenerating = useCallback(() => { if (abortRef.current) {