Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a6cb799e1 | |||
| c33e8f2678 | |||
| 776030832e | |||
| 4c04306d1d | |||
| c7085c72c5 |
+37
-8
@@ -2,12 +2,13 @@
|
|||||||
import httpx
|
import httpx
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
from models.chat import ChatRequest, ChatResponse
|
from models.chat import ChatRequest, ChatResponse
|
||||||
from services import redis_service
|
from services import redis_service
|
||||||
from services.history_manager import prepare_history
|
from services.history_manager import prepare_history, track_lm_call, get_lm_stats
|
||||||
from utils.logging import logger
|
from utils.logging import logger
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
@@ -22,14 +23,15 @@ async def chat_endpoint(payload: ChatRequest):
|
|||||||
meta = redis_service.create_session(payload.user_id, payload.message)
|
meta = redis_service.create_session(payload.user_id, payload.message)
|
||||||
session_id = meta["session_id"]
|
session_id = meta["session_id"]
|
||||||
|
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
|
redis_service.save_chat(payload.user_id, session_id,
|
||||||
|
{"role": "user", "content": payload.message})
|
||||||
history_to_send = await prepare_history(payload.user_id, session_id)
|
history_to_send = await prepare_history(payload.user_id, session_id)
|
||||||
|
|
||||||
#model_to_use = payload.model_name or settings.MODEL_NAME
|
|
||||||
# Recupera modello dalla sessione
|
# Recupera modello dalla sessione
|
||||||
session_meta = redis_service.get_session_meta(payload.user_id, session_id)
|
session_meta = redis_service.get_session_meta(payload.user_id, session_id)
|
||||||
model_to_use = payload.model_name or session_meta.get("model_name") or settings.MODEL_NAME
|
model_to_use = payload.model_name or session_meta.get("model_name") or settings.MODEL_NAME
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
async with httpx.AsyncClient(timeout=settings.REQUEST_TIMEOUT) as client:
|
async with httpx.AsyncClient(timeout=settings.REQUEST_TIMEOUT) as client:
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
settings.LM_STUDIO_URL,
|
settings.LM_STUDIO_URL,
|
||||||
@@ -37,9 +39,17 @@ async def chat_endpoint(payload: ChatRequest):
|
|||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
|
||||||
|
# Traccia la chiamata
|
||||||
|
try:
|
||||||
|
track_lm_call(model_to_use, elapsed)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Errore tracciamento LM Studio: {e}")
|
||||||
|
|
||||||
reply = data["choices"][0]["message"]["content"]
|
reply = data["choices"][0]["message"]["content"]
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": reply})
|
redis_service.save_chat(payload.user_id, session_id,
|
||||||
|
{"role": "assistant", "content": reply})
|
||||||
|
|
||||||
return ChatResponse(response=reply, session_id=session_id)
|
return ChatResponse(response=reply, session_id=session_id)
|
||||||
|
|
||||||
@@ -55,15 +65,17 @@ async def chat_stream_endpoint(payload: ChatRequest):
|
|||||||
meta = redis_service.create_session(payload.user_id, payload.message)
|
meta = redis_service.create_session(payload.user_id, payload.message)
|
||||||
session_id = meta["session_id"]
|
session_id = meta["session_id"]
|
||||||
|
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
|
redis_service.save_chat(payload.user_id, session_id,
|
||||||
|
{"role": "user", "content": payload.message})
|
||||||
history_to_send = await prepare_history(payload.user_id, session_id)
|
history_to_send = await prepare_history(payload.user_id, session_id)
|
||||||
#model_to_use = payload.model_name or settings.MODEL_NAME
|
|
||||||
# Recupera modello dalla sessione
|
# Recupera modello dalla sessione
|
||||||
session_meta = redis_service.get_session_meta(payload.user_id, session_id)
|
session_meta = redis_service.get_session_meta(payload.user_id, session_id)
|
||||||
model_to_use = payload.model_name or session_meta.get("model_name") or settings.MODEL_NAME
|
model_to_use = payload.model_name or session_meta.get("model_name") or settings.MODEL_NAME
|
||||||
|
|
||||||
async def event_generator():
|
async def event_generator():
|
||||||
assistant_text = ""
|
assistant_text = ""
|
||||||
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=None) as client:
|
async with httpx.AsyncClient(timeout=None) as client:
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
@@ -103,15 +115,24 @@ async def chat_stream_endpoint(payload: ChatRequest):
|
|||||||
logger.exception("Streaming error in /chat-stream")
|
logger.exception("Streaming error in /chat-stream")
|
||||||
yield f"event: error\ndata: {str(e)}\n\n"
|
yield f"event: error\ndata: {str(e)}\n\n"
|
||||||
finally:
|
finally:
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
if assistant_text:
|
if assistant_text:
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": assistant_text})
|
redis_service.save_chat(payload.user_id, session_id,
|
||||||
|
{"role": "assistant", "content": assistant_text})
|
||||||
|
# Traccia la chiamata anche per lo stream
|
||||||
|
try:
|
||||||
|
track_lm_call(model_to_use, elapsed)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Errore tracciamento LM Studio: {e}")
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"X-Accel-Buffering": "no"
|
"X-Accel-Buffering": "no"
|
||||||
}
|
}
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
|
return StreamingResponse(event_generator(),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/history")
|
@router.get("/history")
|
||||||
@@ -134,3 +155,11 @@ async def delete_history(
|
|||||||
redis_service.clear_chat(user_id, session_id)
|
redis_service.clear_chat(user_id, session_id)
|
||||||
return {"status": "cleared"}
|
return {"status": "cleared"}
|
||||||
|
|
||||||
|
|
||||||
|
# 📊 Endpoint per statistiche LM Studio
|
||||||
|
@router.get("/lm-stats")
|
||||||
|
async def lm_stats_endpoint():
|
||||||
|
"""
|
||||||
|
Restituisce le statistiche di utilizzo di LM Studio raccolte in Redis.
|
||||||
|
"""
|
||||||
|
return get_lm_stats()
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
# api/v1/chat.py
|
|
||||||
import httpx
|
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from models.chat import ChatRequest, ChatResponse
|
|
||||||
from services import redis_service # now using updated service with session support
|
|
||||||
from utils.logging import logger
|
|
||||||
from config import settings
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
#MAX_HISTORY_LENGTH = 50
|
|
||||||
MAX_HISTORY_LENGTH = 20
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/chat", response_model=ChatResponse)
|
|
||||||
async def chat_endpoint(payload: ChatRequest):
|
|
||||||
try:
|
|
||||||
# Create a new session if session_id not provided
|
|
||||||
session_id = payload.session_id
|
|
||||||
if not session_id:
|
|
||||||
meta = redis_service.create_session(payload.user_id, payload.message)
|
|
||||||
session_id = meta["session_id"]
|
|
||||||
|
|
||||||
# Save user message
|
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
|
|
||||||
history = redis_service.get_chat(payload.user_id, session_id, limit=MAX_HISTORY_LENGTH)
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=settings.REQUEST_TIMEOUT) as client:
|
|
||||||
resp = await client.post(
|
|
||||||
settings.LM_STUDIO_URL,
|
|
||||||
json={"model": settings.MODEL_NAME, "messages": history},
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
|
|
||||||
reply = data["choices"][0]["message"]["content"]
|
|
||||||
|
|
||||||
# Save assistant message
|
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": reply})
|
|
||||||
|
|
||||||
# Return normal ChatResponse, but could also include session_id if needed
|
|
||||||
return ChatResponse(response=reply, session_id=session_id)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Error in /chat endpoint")
|
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/chat-stream")
|
|
||||||
async def chat_stream_endpoint(payload: ChatRequest):
|
|
||||||
"""
|
|
||||||
Streams model output token-by-token using SSE.
|
|
||||||
"""
|
|
||||||
session_id = payload.session_id
|
|
||||||
if not session_id:
|
|
||||||
meta = redis_service.create_session(payload.user_id, payload.message)
|
|
||||||
session_id = meta["session_id"]
|
|
||||||
|
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
|
|
||||||
history = redis_service.get_chat(payload.user_id, session_id, limit=MAX_HISTORY_LENGTH)
|
|
||||||
|
|
||||||
async def event_generator():
|
|
||||||
assistant_text = ""
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=None) as client:
|
|
||||||
async with client.stream(
|
|
||||||
"POST",
|
|
||||||
settings.LM_STUDIO_URL,
|
|
||||||
json={
|
|
||||||
"model": settings.MODEL_NAME,
|
|
||||||
"messages": history,
|
|
||||||
"stream": True
|
|
||||||
}
|
|
||||||
) as r:
|
|
||||||
async for raw_line in r.aiter_lines():
|
|
||||||
if not raw_line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
line = raw_line if raw_line.startswith("data:") else f"data: {raw_line}"
|
|
||||||
payload_str = line[len("data: "):].strip()
|
|
||||||
|
|
||||||
if payload_str == "[DONE]":
|
|
||||||
yield "data: [DONE]\n\n"
|
|
||||||
break
|
|
||||||
|
|
||||||
yield f"data: {payload_str}\n\n"
|
|
||||||
|
|
||||||
try:
|
|
||||||
obj = json.loads(payload_str)
|
|
||||||
choice = obj.get("choices", [{}])[0]
|
|
||||||
delta = choice.get("delta", {})
|
|
||||||
piece = delta.get("content") or choice.get("text")
|
|
||||||
if piece:
|
|
||||||
assistant_text += piece
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Streaming error in /chat-stream")
|
|
||||||
yield f"event: error\ndata: {str(e)}\n\n"
|
|
||||||
finally:
|
|
||||||
if assistant_text:
|
|
||||||
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": assistant_text})
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"X-Accel-Buffering": "no"
|
|
||||||
}
|
|
||||||
return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/history")
|
|
||||||
async def get_history(
|
|
||||||
user_id: str = Query(..., description="User ID"),
|
|
||||||
session_id: str = Query(..., description="Session ID"),
|
|
||||||
limit: int = Query(MAX_HISTORY_LENGTH, description="Max number of messages to return")
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Return all history saved for a given user/session.
|
|
||||||
"""
|
|
||||||
logger.info(f"[GET /history] user_id={user_id}, session_id={session_id}, limit={limit}")
|
|
||||||
history = redis_service.get_chat(user_id, session_id, limit=limit)
|
|
||||||
return history or []
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/history")
|
|
||||||
async def delete_history(
|
|
||||||
user_id: str = Query(..., description="User ID"),
|
|
||||||
session_id: str = Query(..., description="Session ID")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Clears history for a given user/session.
|
|
||||||
"""
|
|
||||||
logger.info(f"[DELETE /history] user_id={user_id}, session_id={session_id}")
|
|
||||||
redis_service.clear_chat(user_id, session_id)
|
|
||||||
return {"status": "cleared"}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,16 +1,85 @@
|
|||||||
# services/history_manager.py
|
# services/history_manager.py
|
||||||
|
import time
|
||||||
import httpx
|
import httpx
|
||||||
from config import settings
|
from config import settings
|
||||||
from services import redis_service
|
from services import redis_service
|
||||||
|
|
||||||
MAX_HISTORY_TURNS = 10 # ultimi turni da mantenere
|
MAX_HISTORY_TURNS = 10
|
||||||
SUMMARY_TRIGGER_TURNS = 20 # soglia per fare summarization
|
SUMMARY_TRIGGER_TURNS = 20
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# STATISTICHE LM STUDIO
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
# services/history_manager.py
|
||||||
|
|
||||||
|
def track_lm_call(model_name: str, elapsed_seconds: float):
|
||||||
|
"""
|
||||||
|
Aggiorna le statistiche di utilizzo LM Studio in Redis, sia globali che per modello.
|
||||||
|
"""
|
||||||
|
model_key = model_name.replace(" ", "_") # per sicurezza nelle chiavi Redis
|
||||||
|
pipe = redis_service.r.pipeline()
|
||||||
|
|
||||||
|
# --- Globali ---
|
||||||
|
pipe.incr("lm:calls:total")
|
||||||
|
pipe.incr("lm:calls:last_hour")
|
||||||
|
pipe.expire("lm:calls:last_hour", 3600)
|
||||||
|
pipe.incr("lm:calls:last_24h")
|
||||||
|
pipe.expire("lm:calls:last_24h", 86400)
|
||||||
|
pipe.incrbyfloat("lm:processing_time:total", elapsed_seconds)
|
||||||
|
|
||||||
|
# --- Per modello ---
|
||||||
|
pipe.sadd("lm:models:loaded", model_name)
|
||||||
|
|
||||||
|
# Chiamate
|
||||||
|
pipe.incr(f"lm:model:{model_key}:calls:last_hour")
|
||||||
|
pipe.expire(f"lm:model:{model_key}:calls:last_hour", 3600)
|
||||||
|
pipe.incr(f"lm:model:{model_key}:calls:last_24h")
|
||||||
|
pipe.expire(f"lm:model:{model_key}:calls:last_24h", 86400)
|
||||||
|
|
||||||
|
# Tempo totale
|
||||||
|
pipe.incrbyfloat(f"lm:model:{model_key}:time:total", elapsed_seconds)
|
||||||
|
|
||||||
|
pipe.execute()
|
||||||
|
|
||||||
|
def get_lm_stats():
|
||||||
|
"""
|
||||||
|
Restituisce statistiche globali e per modello.
|
||||||
|
"""
|
||||||
|
models = list(redis_service.r.smembers("lm:models:loaded"))
|
||||||
|
stats_per_model = []
|
||||||
|
|
||||||
|
for m in models:
|
||||||
|
model_key = m.replace(" ", "_")
|
||||||
|
calls_last_hour = int(redis_service.r.get(f"lm:model:{model_key}:calls:last_hour") or 0)
|
||||||
|
calls_last_24h = int(redis_service.r.get(f"lm:model:{model_key}:calls:last_24h") or 0)
|
||||||
|
total_time = float(redis_service.r.get(f"lm:model:{model_key}:time:total") or 0.0)
|
||||||
|
avg_time = (total_time / calls_last_24h) if calls_last_24h > 0 else 0.0
|
||||||
|
|
||||||
|
stats_per_model.append({
|
||||||
|
"model": m,
|
||||||
|
"calls_last_hour": calls_last_hour,
|
||||||
|
"calls_last_24h": calls_last_24h,
|
||||||
|
"total_time_sec": total_time,
|
||||||
|
"avg_time_sec": avg_time
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"global": {
|
||||||
|
"calls_total": int(redis_service.r.get("lm:calls:total") or 0),
|
||||||
|
"calls_last_hour": int(redis_service.r.get("lm:calls:last_hour") or 0),
|
||||||
|
"calls_last_24h": int(redis_service.r.get("lm:calls:last_24h") or 0),
|
||||||
|
"total_processing_time_sec": float(redis_service.r.get("lm:processing_time:total") or 0.0)
|
||||||
|
},
|
||||||
|
"per_model": stats_per_model
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# HISTORY
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
async def summarize_messages(messages):
|
async def summarize_messages(messages):
|
||||||
"""
|
|
||||||
Usa LM Studio per riassumere i messaggi in forma compatta (condensed history).
|
|
||||||
"""
|
|
||||||
prompt = [
|
prompt = [
|
||||||
{"role": "system", "content": "Riassumi la seguente conversazione in forma di elenco puntato, mantenendo solo i fatti chiave e le informazioni rilevanti per continuare il dialogo."},
|
{"role": "system", "content": "Riassumi la seguente conversazione in forma di elenco puntato, mantenendo solo i fatti chiave e le informazioni rilevanti per continuare il dialogo."},
|
||||||
*messages
|
*messages
|
||||||
@@ -26,10 +95,6 @@ async def summarize_messages(messages):
|
|||||||
|
|
||||||
|
|
||||||
async def prepare_history(user_id: str, session_id: str):
|
async def prepare_history(user_id: str, session_id: str):
|
||||||
"""
|
|
||||||
Recupera la history da Redis, applica windowing e summarization se necessario,
|
|
||||||
e restituisce la lista di messaggi da passare al modello.
|
|
||||||
"""
|
|
||||||
full_history = redis_service.get_chat(user_id, session_id, limit=1000)
|
full_history = redis_service.get_chat(user_id, session_id, limit=1000)
|
||||||
|
|
||||||
if len(full_history) > SUMMARY_TRIGGER_TURNS:
|
if len(full_history) > SUMMARY_TRIGGER_TURNS:
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
// 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";
|
|
||||||
|
|
||||||
// Prism.js per syntax highlight
|
|
||||||
import Prism from "prismjs";
|
|
||||||
import "prismjs/themes/prism.css";
|
|
||||||
|
|
||||||
// Linguaggi base
|
|
||||||
import "prismjs/components/prism-sql";
|
|
||||||
import "prismjs/components/prism-javascript";
|
|
||||||
import "prismjs/components/prism-css";
|
|
||||||
import "prismjs/components/prism-json";
|
|
||||||
import "prismjs/components/prism-markdown";
|
|
||||||
import "prismjs/components/prism-csharp";
|
|
||||||
import "prismjs/components/prism-lua";
|
|
||||||
import "prismjs/components/prism-c";
|
|
||||||
import "prismjs/components/prism-cpp";
|
|
||||||
import "prismjs/components/prism-python";
|
|
||||||
import "prismjs/components/prism-basic";
|
|
||||||
import "prismjs/components/prism-javascript";
|
|
||||||
|
|
||||||
// Aggiungo alias "vb" che punta a "vbnet"
|
|
||||||
Prism.languages.vb = Prism.languages.vbnet;
|
|
||||||
|
|
||||||
export default function AssistantMessage({
|
|
||||||
content,
|
|
||||||
theme,
|
|
||||||
timestamp,
|
|
||||||
startedAt,
|
|
||||||
endedAt,
|
|
||||||
isFinal
|
|
||||||
}) {
|
|
||||||
const [showThink, setShowThink] = useState(false);
|
|
||||||
const [fadeOut, setFadeOut] = useState(false);
|
|
||||||
|
|
||||||
const ts = timestamp ?? endedAt ?? startedAt;
|
|
||||||
|
|
||||||
const thinkMatch = content?.match(/<think>([\s\S]*?)<\/think>/i);
|
|
||||||
const thinkContent = thinkMatch ? thinkMatch[1].trim() : null;
|
|
||||||
|
|
||||||
const isComplete = isFinal || Boolean(timestamp || endedAt);
|
|
||||||
|
|
||||||
const visibleContent = isComplete
|
|
||||||
? content?.replace(/<think>[\s\S]*?<\/think>/i, "").trim()
|
|
||||||
: content;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (thinkContent && !isComplete) {
|
|
||||||
setShowThink(true);
|
|
||||||
setFadeOut(false);
|
|
||||||
}
|
|
||||||
if (thinkContent && isComplete) {
|
|
||||||
setFadeOut(true);
|
|
||||||
const timer = setTimeout(() => setShowThink(false), 600);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [thinkContent, isComplete]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-2 text-start">
|
|
||||||
<div
|
|
||||||
className={`d-inline-block p-2 rounded ${theme.assistantBg}`}
|
|
||||||
style={{ maxWidth: "75%" }}
|
|
||||||
>
|
|
||||||
{showThink && (
|
|
||||||
<div
|
|
||||||
className={`think-block${fadeOut ? " fade-out" : ""}`}
|
|
||||||
style={{
|
|
||||||
fontStyle: "italic",
|
|
||||||
opacity: 0.7,
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
whiteSpace: "pre-wrap"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🤔 {thinkContent}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
|
||||||
rehypePlugins={[rehypeKatex]}
|
|
||||||
components={{
|
|
||||||
table: (props) => (
|
|
||||||
<table className="table table-sm table-bordered" {...props} />
|
|
||||||
),
|
|
||||||
th: (props) => <th className="bg-light" {...props} />,
|
|
||||||
code: CodeWithCopy
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{visibleContent}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
{ts != null && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
color: "#666",
|
|
||||||
marginTop: "0.2rem",
|
|
||||||
marginLeft: "0.25rem"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatDateTime(ts)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeWithCopy({ inline, className = "", children, ...props }) {
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const codeText = String(children).replace(/\n$/, "");
|
|
||||||
const isFencedBlock = !inline && /^language-/.test(className);
|
|
||||||
|
|
||||||
// Evidenziazione con Prism
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFencedBlock) {
|
|
||||||
Prism.highlightAll();
|
|
||||||
}
|
|
||||||
}, [codeText, isFencedBlock]);
|
|
||||||
|
|
||||||
if (!isFencedBlock) {
|
|
||||||
return (
|
|
||||||
<code className={className} {...props}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(codeText);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 1500);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Copy failed", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ position: "relative" }}>
|
|
||||||
<pre className={className} {...props} style={{ paddingRight: "2rem" }}>
|
|
||||||
<code className={className}>{codeText}</code>
|
|
||||||
</pre>
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "0.25rem",
|
|
||||||
right: "0.25rem",
|
|
||||||
border: "none",
|
|
||||||
background: "transparent",
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
className="btn btn-copy shadow"
|
|
||||||
title="Copy to clipboard"
|
|
||||||
>
|
|
||||||
📋
|
|
||||||
</button>
|
|
||||||
{copied && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "0.25rem",
|
|
||||||
right: "2rem",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
color: "green"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copied!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateTime(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"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -73,9 +73,9 @@ export default function ChatHeader({
|
|||||||
{/* Center column: titolo solo se c'è sessione */}
|
{/* Center column: titolo solo se c'è sessione */}
|
||||||
<div className="col-5 text-center">
|
<div className="col-5 text-center">
|
||||||
<h4 className="mb-0" title={`Default model: ${defaultModelName}`}>
|
<h4 className="mb-0" title={`Default model: ${defaultModelName}`}>
|
||||||
🤖 EgalWare's LLM ChatBot
|
🤖 EgalWare's ChatBot
|
||||||
<button className="ms-2 btn btn-sm btn-outline-info" onClick={onToggleModels}>
|
<button className="ms-2 btn btn-sm btn-outline-info" onClick={onToggleModels}>
|
||||||
📊 Modelli
|
📊 LLM Models
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+45
-45
@@ -70,56 +70,56 @@ export default function ChatLayout({
|
|||||||
onToggleModels={toggleModelsDetail}
|
onToggleModels={toggleModelsDetail}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
sessionName={sessionName}
|
sessionName={sessionName}
|
||||||
defaultModelName={defaultModelName}
|
defaultModelName={defaultModelName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showModelPage ? (
|
<div className="flex-grow-1 overflow-auto" style={{ minHeight: 0 }}>
|
||||||
<ModelOverview onBackToChat={() => setShowModelPage(false)} />
|
{showModelPage ? (
|
||||||
) : (
|
<ModelOverview onBackToChat={() => setShowModelPage(false)} />
|
||||||
/* AREA SCROLLABILE */
|
) : (
|
||||||
<div className="flex-grow-1 overflow-auto" style={{ minHeight: 0 }}>
|
/* AREA SCROLLABILE */
|
||||||
<ChatWindow messages={messages} loading={loading} theme={theme} />
|
<ChatWindow messages={messages} loading={loading} theme={theme} />
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* INPUT SEMPRE IN BASSO */}
|
|
||||||
{!showModelPage && (
|
|
||||||
sessionId ? (
|
|
||||||
<ChatInput
|
|
||||||
onSend={(message, modelName) => onSend(message, modelName)}
|
|
||||||
onStop={onStop}
|
|
||||||
loading={loading}
|
|
||||||
sessionModelName={sessionModelName || ""}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NoSessionBox onCreateSession={onCreateSession} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PANEL SESSIONI */}
|
{/* INPUT SEMPRE IN BASSO */}
|
||||||
<div
|
{!showModelPage && (
|
||||||
className={`offcanvas offcanvas-start ${showSessionsPanel ? "show" : ""}`}
|
sessionId ? (
|
||||||
tabIndex="-1"
|
<ChatInput
|
||||||
style={{ visibility: showSessionsPanel ? "visible" : "hidden" }}
|
onSend={(message, modelName) => onSend(message, modelName)}
|
||||||
>
|
onStop={onStop}
|
||||||
<div className="offcanvas-header">
|
loading={loading}
|
||||||
<h5 className="offcanvas-title">Manage Sessions</h5>
|
sessionModelName={sessionModelName || ""}
|
||||||
<button
|
/>
|
||||||
type="button"
|
) : (
|
||||||
className="btn-close text-reset"
|
<NoSessionBox onCreateSession={onCreateSession} />
|
||||||
onClick={closeSessionManager}
|
)
|
||||||
></button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="offcanvas-body">
|
|
||||||
<SessionTable
|
{/* PANEL SESSIONI */}
|
||||||
userId={userId}
|
<div
|
||||||
onSelectSession={onSelectSession}
|
className={`offcanvas offcanvas-start ${showSessionsPanel ? "show" : ""}`}
|
||||||
onClosePanel={closeSessionManager}
|
tabIndex="-1"
|
||||||
/>
|
style={{ visibility: showSessionsPanel ? "visible" : "hidden" }}
|
||||||
</div>
|
>
|
||||||
|
<div className="offcanvas-header">
|
||||||
|
<h5 className="offcanvas-title">Manage Sessions</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close text-reset"
|
||||||
|
onClick={closeSessionManager}
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="offcanvas-body">
|
||||||
|
<SessionTable
|
||||||
|
userId={userId}
|
||||||
|
onSelectSession={onSelectSession}
|
||||||
|
onClosePanel={closeSessionManager}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// src/LmStats.jsx
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function LmStats() {
|
||||||
|
const [stats, setStats] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/v1/lm-stats"); // Adatta se API su dominio diverso
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setStats(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Errore caricamento stats:", err);
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadStats();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="alert alert-info">Caricamento statistiche...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="alert alert-danger">Errore: {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stats) {
|
||||||
|
return <div className="alert alert-warning">Nessun dato disponibile</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container my-4">
|
||||||
|
<h2 className="mb-4">📊 Statistiche LM Studio</h2>
|
||||||
|
|
||||||
|
{/* Statistiche globali */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header bg-primary text-white">
|
||||||
|
🌍 Globali
|
||||||
|
</div>
|
||||||
|
<div className="card-body p-0">
|
||||||
|
<table className="table table-striped mb-0">
|
||||||
|
<thead className="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Chiamate totali</th>
|
||||||
|
<th>Ultima ora</th>
|
||||||
|
<th>Ultime 24h</th>
|
||||||
|
<th>Tempo totale (s)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{stats.global.calls_total}</td>
|
||||||
|
<td>{stats.global.calls_last_hour}</td>
|
||||||
|
<td>{stats.global.calls_last_24h}</td>
|
||||||
|
<td>{stats.global.total_processing_time_sec.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Statistiche per modello */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header bg-success text-white">
|
||||||
|
🧠 Per Modello
|
||||||
|
</div>
|
||||||
|
<div className="card-body p-0">
|
||||||
|
<table className="table table-hover mb-0">
|
||||||
|
<thead className="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Modello</th>
|
||||||
|
<th>Ultima ora</th>
|
||||||
|
<th>Ultime 24h</th>
|
||||||
|
<th>Tempo totale (s)</th>
|
||||||
|
<th>Tempo medio (s)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{stats.per_model.map((m) => (
|
||||||
|
<tr key={m.model}>
|
||||||
|
<td>{m.model}</td>
|
||||||
|
<td>{m.calls_last_hour}</td>
|
||||||
|
<td>{m.calls_last_24h}</td>
|
||||||
|
<td>{m.total_time_sec.toFixed(2)}</td>
|
||||||
|
<td>{m.avg_time_sec.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+121
-63
@@ -1,12 +1,25 @@
|
|||||||
// src/ModelOverview.jsx
|
// src/ModelOverview.jsx
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import LmStats from "./LmStats";
|
||||||
|
|
||||||
export default function ModelOverview({ onBackToChat }) {
|
export default function ModelOverview({ onBackToChat }) {
|
||||||
const [defaultModel, setDefaultModel] = useState("");
|
const [defaultModel, setDefaultModel] = useState("");
|
||||||
const [modelInfo, setModelInfo] = useState([]);
|
const [modelInfo, setModelInfo] = useState([]);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showStats, setShowStats] = useState(false);
|
||||||
|
const [sortColumn, setSortColumn] = useState("name");
|
||||||
|
const [sortDirection, setSortDirection] = useState("asc");
|
||||||
|
|
||||||
|
const handleSort = (column) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
|
||||||
|
} else {
|
||||||
|
setSortColumn(column);
|
||||||
|
setSortDirection("asc");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchModelData = async () => {
|
const fetchModelData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -45,77 +58,122 @@ export default function ModelOverview({ onBackToChat }) {
|
|||||||
fetchModelData();
|
fetchModelData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredModels = modelInfo.filter((model) =>
|
const filteredModels = modelInfo
|
||||||
|
.filter((model) =>
|
||||||
model.name.toLowerCase().includes(searchTerm.toLowerCase())
|
model.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const valA = a[sortColumn] || "";
|
||||||
|
const valB = b[sortColumn] || "";
|
||||||
|
if (typeof valA === "string" && typeof valB === "string") {
|
||||||
|
return sortDirection === "asc"
|
||||||
|
? valA.localeCompare(valB)
|
||||||
|
: valB.localeCompare(valA);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container py-4">
|
<div className="container py-4">
|
||||||
|
{/* Jumbotron con layout a due colonne */}
|
||||||
<div className="bg-light p-4 rounded shadow-sm mb-4">
|
<div className="bg-light p-4 rounded shadow-sm mb-4">
|
||||||
<h3 className="mb-0">📌 Modello di default</h3>
|
<div className="row align-items-center">
|
||||||
<p className="lead text-muted mb-0">
|
{/* Sinistra: modello di default */}
|
||||||
{defaultModel || "Non disponibile"}
|
<div className="col-6 col-md-4">
|
||||||
</p>
|
<h3 className="mb-2">📌 Modello di default</h3>
|
||||||
|
<p className="lead text-muted border border-info border-3 rounded-3 mb-0 text-center shadow">
|
||||||
|
{defaultModel || "Non disponibile"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Destra: pulsanti su due righe */}
|
||||||
|
<div className="col-6 col-md-8 text-md-end mt-3 mt-md-0">
|
||||||
|
<div className="d-grid gap-2 d-md-flex justify-content-md-end mb-2">
|
||||||
|
<button
|
||||||
|
className={`btn ${showStats ? "btn-secondary" : "btn-outline-secondary"}`}
|
||||||
|
onClick={() => setShowStats(prev => !prev)}
|
||||||
|
>
|
||||||
|
{showStats ? "📋 Modelli Disponibili" : "📊 Statistiche Uso"}
|
||||||
|
</button>
|
||||||
|
{/* </div>
|
||||||
|
<div className="d-grid gap-2 d-md-flex justify-content-md-end"> */}
|
||||||
|
<button className="btn btn-outline-primary" onClick={onBackToChat}>
|
||||||
|
⬅ Torna alla chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex justify-content-between mb-3">
|
{/* Contenuto principale */}
|
||||||
<button className="btn btn-outline-primary" onClick={onBackToChat}>
|
{showStats ? (
|
||||||
⬅ Torna alla chat
|
<LmStats />
|
||||||
</button>
|
) : (
|
||||||
<button className="btn btn-outline-success" onClick={updateModelData}>
|
<>
|
||||||
🔄 Aggiorna dati modelli
|
{/* InputGroup: cerca + aggiorna */}
|
||||||
</button>
|
<div className="mb-3">
|
||||||
</div>
|
<div className="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="🔍 Cerca modello per nome..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-success"
|
||||||
|
type="button"
|
||||||
|
onClick={updateModelData}
|
||||||
|
>
|
||||||
|
🔄 Aggiorna info
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="table-responsive">
|
||||||
<input
|
<table className="table table-bordered table-hover align-middle">
|
||||||
type="text"
|
<thead className="table-light">
|
||||||
className="form-control"
|
<tr>
|
||||||
placeholder="🔍 Cerca modello per nome..."
|
<th onClick={() => handleSort("name")} style={{ cursor: "pointer" }}>
|
||||||
value={searchTerm}
|
Nome {sortColumn === "name" && (sortDirection === "asc" ? "⬆" : "⬇")}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
</th>
|
||||||
/>
|
<th onClick={() => handleSort("description")} style={{ cursor: "pointer" }}>
|
||||||
</div>
|
Descrizione {sortColumn === "description" && (sortDirection === "asc" ? "⬆" : "⬇")}
|
||||||
|
</th>
|
||||||
<div className="table-responsive">
|
<th onClick={() => handleSort("features")} style={{ cursor: "pointer" }}>
|
||||||
<table className="table table-bordered table-hover align-middle">
|
Caratteristiche {sortColumn === "features" && (sortDirection === "asc" ? "⬆" : "⬇")}
|
||||||
<thead className="table-light">
|
</th>
|
||||||
<tr>
|
</tr>
|
||||||
<th>Nome</th>
|
</thead>
|
||||||
<th>Descrizione</th>
|
<tbody>
|
||||||
{/*<th>Anno</th>*/}
|
{loading && (
|
||||||
<th>Caratteristiche</th>
|
<tr>
|
||||||
</tr>
|
<td colSpan="3" className="text-center">Caricamento…</td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
)}
|
||||||
{loading && (
|
{!loading && filteredModels.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="4" className="text-center">Caricamento…</td>
|
<td colSpan="3" className="text-center">Nessun modello trovato</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!loading && filteredModels.length === 0 && (
|
{!loading && filteredModels.map((model) => (
|
||||||
<tr>
|
<tr key={model.name}>
|
||||||
<td colSpan="4" className="text-center">Nessun modello trovato</td>
|
<td>{model.name}</td>
|
||||||
</tr>
|
<td>{model.description || "—"}</td>
|
||||||
)}
|
<td>
|
||||||
{!loading && filteredModels.map((model) => (
|
<ul className="mb-0 small">
|
||||||
<tr key={model.name}>
|
{Array.isArray(model.features) && model.features.length > 0
|
||||||
<td>{model.name}</td>
|
? model.features.map((f, i) => <li key={i}>{f}</li>)
|
||||||
<td>{model.description || "—"}</td>
|
: <li>—</li>}
|
||||||
{/*<td>{model.year || "—"}</td>*/}
|
</ul>
|
||||||
<td>
|
</td>
|
||||||
<ul className="mb-0 small">
|
</tr>
|
||||||
{Array.isArray(model.features) && model.features.length > 0
|
))}
|
||||||
? model.features.map((f, i) => <li key={i}>{f}</li>)
|
</tbody>
|
||||||
: <li>—</li>}
|
</table>
|
||||||
</ul>
|
</div>
|
||||||
</td>
|
</>
|
||||||
</tr>
|
)}
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
// src/ModelOverview.jsx
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function ModelOverview({ onBackToChat }) {
|
|
||||||
const [defaultModel, setDefaultModel] = useState("");
|
|
||||||
const [modelInfo, setModelInfo] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const fetchModelData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const [defaultRes, infoRes] = await Promise.all([
|
|
||||||
fetch("/v1/default-model"),
|
|
||||||
fetch("/v1/models-info")
|
|
||||||
]);
|
|
||||||
const defaultText = await defaultRes.text();
|
|
||||||
const infoJson = await infoRes.json();
|
|
||||||
setDefaultModel(defaultText);
|
|
||||||
setModelInfo(infoJson);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Errore nel caricamento modelli", err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateModelData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch("/v1/models-info/update", { method: "POST" });
|
|
||||||
if (res.ok) {
|
|
||||||
const updated = await res.json();
|
|
||||||
setModelInfo(updated);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Errore nell'aggiornamento modelli", err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchModelData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container py-4">
|
|
||||||
<div className="bg-light p-4 rounded shadow-sm mb-4">
|
|
||||||
<h3 className="mb-0">📌 Modello di default</h3>
|
|
||||||
<p className="lead text-muted mb-0">
|
|
||||||
{defaultModel || "Non disponibile"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-between mb-3">
|
|
||||||
<button className="btn btn-outline-primary" onClick={onBackToChat}>
|
|
||||||
⬅ Torna alla chat
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-outline-success" onClick={updateModelData}>
|
|
||||||
🔄 Aggiorna dati modelli
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table table-bordered table-hover align-middle">
|
|
||||||
<thead className="table-light">
|
|
||||||
<tr>
|
|
||||||
<th>Nome</th>
|
|
||||||
<th>Descrizione</th>
|
|
||||||
<th>Anno</th>
|
|
||||||
<th>Caratteristiche</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{loading && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan="4" className="text-center">Caricamento…</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{!loading && modelInfo.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan="4" className="text-center">Nessun modello disponibile</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{!loading && modelInfo.map((model) => (
|
|
||||||
<tr key={model.name}>
|
|
||||||
<td>{model.name}</td>
|
|
||||||
<td>{model.description || "—"}</td>
|
|
||||||
<td>{model.year || "—"}</td>
|
|
||||||
<td>
|
|
||||||
<ul className="mb-0 small">
|
|
||||||
{model.features?.map((f, i) => (
|
|
||||||
<li key={i}>{f}</li>
|
|
||||||
)) || <li>—</li>}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user