[\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) {