diff --git a/fastapi_server/server_viewer.py b/fastapi_server/server_viewer.py
new file mode 100644
index 0000000..f41130f
--- /dev/null
+++ b/fastapi_server/server_viewer.py
@@ -0,0 +1,130 @@
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import HTMLResponse, JSONResponse
+from fastapi.staticfiles import StaticFiles
+from pydantic import BaseModel
+from typing import List, Dict, Optional
+import os
+import sys
+from pathlib import Path
+from dotenv import load_dotenv
+
+load_dotenv()
+
+# Ensure we can import from project root
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from lang_agent.components.conv_store import ConversationStore
+
+app = FastAPI(
+ title="Conversation Viewer",
+ description="Web UI to view conversations from the database",
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Initialize conversation store
+try:
+ conv_store = ConversationStore()
+except ValueError as e:
+ print(f"Warning: {e}. Make sure CONN_STR environment variable is set.")
+ conv_store = None
+
+
+class MessageResponse(BaseModel):
+ message_type: str
+ content: str
+ sequence_number: int
+ created_at: str
+
+
+class ConversationListItem(BaseModel):
+ conversation_id: str
+ message_count: int
+ last_updated: Optional[str] = None
+
+
+@app.get("/", response_class=HTMLResponse)
+async def root():
+ """Serve the main HTML page"""
+ html_path = Path(__file__).parent.parent / "static" / "viewer.html"
+ if html_path.exists():
+ return HTMLResponse(content=html_path.read_text(encoding="utf-8"))
+ else:
+ return HTMLResponse(content="
Viewer HTML not found. Please create static/viewer.html
")
+
+
+@app.get("/api/conversations", response_model=List[ConversationListItem])
+async def list_conversations():
+ """Get list of all conversations"""
+ if conv_store is None:
+ raise HTTPException(status_code=500, detail="Database connection not configured")
+
+ import psycopg
+ conn_str = os.environ.get("CONN_STR")
+ if not conn_str:
+ raise HTTPException(status_code=500, detail="CONN_STR not set")
+
+ with psycopg.connect(conn_str) as conn:
+ with conn.cursor(row_factory=psycopg.rows.dict_row) as cur:
+ # Get all unique conversation IDs with message counts and last updated time
+ cur.execute("""
+ SELECT
+ conversation_id,
+ COUNT(*) as message_count,
+ MAX(created_at) as last_updated
+ FROM messages
+ GROUP BY conversation_id
+ ORDER BY last_updated DESC
+ """)
+ results = cur.fetchall()
+
+ return [
+ ConversationListItem(
+ conversation_id=row["conversation_id"],
+ message_count=row["message_count"],
+ last_updated=row["last_updated"].isoformat() if row["last_updated"] else None
+ )
+ for row in results
+ ]
+
+
+@app.get("/api/conversations/{conversation_id}/messages", response_model=List[MessageResponse])
+async def get_conversation_messages(conversation_id: str):
+ """Get all messages for a specific conversation"""
+ if conv_store is None:
+ raise HTTPException(status_code=500, detail="Database connection not configured")
+
+ messages = conv_store.get_conversation(conversation_id)
+
+ return [
+ MessageResponse(
+ message_type=msg["message_type"],
+ content=msg["content"],
+ sequence_number=msg["sequence_number"],
+ created_at=msg["created_at"].isoformat() if msg["created_at"] else ""
+ )
+ for msg in messages
+ ]
+
+
+@app.get("/health")
+async def health():
+ return {"status": "healthy", "db_connected": conv_store is not None}
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(
+ "server_viewer:app",
+ host="0.0.0.0",
+ port=8590,
+ reload=True,
+ )
+
diff --git a/static/viewer.html b/static/viewer.html
new file mode 100644
index 0000000..63672e1
--- /dev/null
+++ b/static/viewer.html
@@ -0,0 +1,394 @@
+
+
+
+
+
+ Conversation Viewer
+
+
+
+
+
+
+
+
+
+
+
+
👈
+
Select a conversation from the left to view messages
+
+
+
+
+
+
+
+