GraphResearcher / scripts /phase25_chatgpt_style_app_ui.py
yugbirla's picture
Add ChatGPT style product app UI
0acc4ab
Raw
History Blame Contribute Delete
18.3 kB
from pathlib import Path
# Clean BOM
for path in Path("app").rglob("*.py"):
text = path.read_text(encoding="utf-8-sig")
text = text.replace("\ufeff", "")
path.write_text(text, encoding="utf-8")
print("BOM cleanup completed.")
hf_path = Path("app/deployment/hf_status.py")
text = hf_path.read_text(encoding="utf-8-sig")
text = text.replace("\ufeff", "")
product_app_html = r'''
def get_product_app_html() -> str:
return """
<!DOCTYPE html>
<html>
<head>
<title>GraphResearcher App</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Inter, Arial, sans-serif;
background: #f8fafc;
color: #111827;
}
.app {
display: grid;
grid-template-columns: 280px 1fr 360px;
height: 100vh;
}
.sidebar {
background: #0f172a;
color: white;
padding: 16px;
overflow-y: auto;
}
.sidebar h2 {
margin-top: 0;
font-size: 20px;
}
.new-chat {
width: 100%;
background: #2563eb;
color: white;
border: none;
border-radius: 10px;
padding: 11px;
cursor: pointer;
margin-bottom: 16px;
font-size: 14px;
}
.conversation {
padding: 10px;
border-radius: 9px;
cursor: pointer;
margin-bottom: 8px;
background: rgba(255,255,255,0.07);
font-size: 14px;
}
.conversation.active {
background: #2563eb;
}
.main {
display: flex;
flex-direction: column;
height: 100vh;
}
.topbar {
padding: 14px 20px;
border-bottom: 1px solid #e5e7eb;
background: white;
display: flex;
gap: 10px;
align-items: center;
}
.topbar input {
flex: 1;
padding: 10px;
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.status-pill {
font-size: 12px;
padding: 6px 9px;
border-radius: 999px;
background: #e0f2fe;
color: #075985;
white-space: nowrap;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.message {
max-width: 850px;
margin-bottom: 18px;
display: flex;
}
.message.user {
margin-left: auto;
justify-content: flex-end;
}
.bubble {
padding: 14px 16px;
border-radius: 16px;
line-height: 1.55;
white-space: pre-wrap;
}
.user .bubble {
background: #2563eb;
color: white;
border-bottom-right-radius: 4px;
}
.assistant .bubble {
background: white;
border: 1px solid #e5e7eb;
color: #111827;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.composer {
padding: 16px 20px;
background: white;
border-top: 1px solid #e5e7eb;
}
.composer-box {
display: flex;
gap: 10px;
align-items: flex-end;
}
textarea {
flex: 1;
resize: none;
min-height: 54px;
max-height: 160px;
padding: 13px;
border: 1px solid #cbd5e1;
border-radius: 12px;
font-size: 15px;
}
button {
background: #2563eb;
color: white;
border: none;
border-radius: 10px;
padding: 12px 16px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #1d4ed8;
}
button.secondary {
background: #334155;
}
button.green {
background: #059669;
}
.right-panel {
background: white;
border-left: 1px solid #e5e7eb;
padding: 16px;
overflow-y: auto;
}
.panel-section {
margin-bottom: 18px;
}
.panel-section h3 {
margin-bottom: 8px;
font-size: 16px;
}
.toggle-row {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 14px;
}
.citation-card {
border: 1px solid #e5e7eb;
background: #f8fafc;
border-radius: 10px;
padding: 10px;
margin-bottom: 10px;
font-size: 13px;
}
.metric {
display: inline-block;
background: #eef2ff;
color: #3730a3;
padding: 5px 8px;
border-radius: 999px;
font-size: 12px;
margin: 3px;
}
.small {
font-size: 12px;
color: #64748b;
}
.hidden {
display: none;
}
@media (max-width: 1050px) {
.app {
grid-template-columns: 240px 1fr;
}
.right-panel {
display: none;
}
}
@media (max-width: 760px) {
.app {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<h2>GraphResearcher</h2>
<button class="new-chat" onclick="newChat()">+ New chat</button>
<div class="small" style="margin-bottom: 10px;">Conversations</div>
<div id="conversationList"></div>
<hr style="border-color: rgba(255,255,255,0.15); margin: 18px 0;">
<button class="new-chat secondary" onclick="window.open('/graphrag-demo','_blank')">GraphRAG Console</button>
<button class="new-chat secondary" onclick="window.open('/docs','_blank')">Swagger Docs</button>
</aside>
<main class="main">
<div class="topbar">
<input id="documentId" placeholder="Paste document_id here">
<span id="appStatus" class="status-pill">Ready</span>
</div>
<div id="messages" class="messages"></div>
<div class="composer">
<div class="composer-box">
<textarea id="messageInput" placeholder="Ask anything about your document..." onkeydown="handleKeyDown(event)"></textarea>
<button onclick="sendMessage()">Send</button>
</div>
<div class="small" style="margin-top: 8px;">
Tip: ask follow-ups like “explain it simply” or “how is it different?” The app sends recent chat context with your question.
</div>
</div>
</main>
<aside class="right-panel">
<div class="panel-section">
<h3>GraphRAG Settings</h3>
<div class="toggle-row">
<label><input type="checkbox" id="useLLM" checked> Use LLM</label>
<label><input type="checkbox" id="useReranker" checked> Use reranker</label>
<label><input type="checkbox" id="useGraph" checked> Use graph context</label>
<label><input type="checkbox" id="useGraphRetrieval" checked> Use graph retrieval fusion</label>
</div>
</div>
<div class="panel-section">
<h3>Actions</h3>
<button class="green" onclick="buildGraph()">Build Graph</button>
<button class="secondary" onclick="openGraph()">Open Graph</button>
</div>
<div class="panel-section">
<h3>Last Answer Metrics</h3>
<div id="metricsBox">
<span class="metric">No answer yet</span>
</div>
</div>
<div class="panel-section">
<h3>Citations</h3>
<div id="citationsBox" class="small">Citations will appear here.</div>
</div>
</aside>
</div>
<script>
let conversations = JSON.parse(localStorage.getItem("graphrag_conversations") || "[]");
let activeConversationId = localStorage.getItem("graphrag_active_conversation");
function saveState() {
localStorage.setItem("graphrag_conversations", JSON.stringify(conversations));
if (activeConversationId) {
localStorage.setItem("graphrag_active_conversation", activeConversationId);
}
}
function getActiveConversation() {
let convo = conversations.find(c => c.id === activeConversationId);
if (!convo) {
convo = {
id: crypto.randomUUID(),
title: "New chat",
messages: [],
createdAt: new Date().toISOString()
};
conversations.unshift(convo);
activeConversationId = convo.id;
saveState();
}
return convo;
}
function newChat() {
const convo = {
id: crypto.randomUUID(),
title: "New chat",
messages: [],
createdAt: new Date().toISOString()
};
conversations.unshift(convo);
activeConversationId = convo.id;
saveState();
renderConversations();
renderMessages();
}
function selectConversation(id) {
activeConversationId = id;
saveState();
renderConversations();
renderMessages();
}
function renderConversations() {
const list = document.getElementById("conversationList");
list.innerHTML = "";
conversations.forEach(convo => {
const div = document.createElement("div");
div.className = "conversation" + (convo.id === activeConversationId ? " active" : "");
div.textContent = convo.title || "New chat";
div.onclick = () => selectConversation(convo.id);
list.appendChild(div);
});
}
function renderMessages() {
const convo = getActiveConversation();
const box = document.getElementById("messages");
box.innerHTML = "";
if (convo.messages.length === 0) {
box.innerHTML = `
<div class="message assistant">
<div class="bubble">
Hi, I am your GraphRAG research assistant. Paste a document_id above and ask a question.
I can answer with citations, graph context, and retrieval fusion.
</div>
</div>
`;
return;
}
convo.messages.forEach(msg => {
const wrapper = document.createElement("div");
wrapper.className = "message " + msg.role;
const bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = msg.content;
wrapper.appendChild(bubble);
box.appendChild(wrapper);
});
box.scrollTop = box.scrollHeight;
}
function setStatus(text) {
document.getElementById("appStatus").textContent = text;
}
function handleKeyDown(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
function buildContextualQuery(currentQuestion) {
const convo = getActiveConversation();
const recentMessages = convo.messages.slice(-6);
if (recentMessages.length === 0) {
return currentQuestion;
}
const context = recentMessages
.map(m => `${m.role}: ${m.content}`)
.join("\\n");
return `Conversation so far:\\n${context}\\n\\nCurrent user question:\\n${currentQuestion}`;
}
function updateMetrics(data) {
const fusion = data.retrieval_fusion || {};
const graph = data.graph_context || {};
document.getElementById("metricsBox").innerHTML = `
<span class="metric">strategy: ${data.answer_strategy || "NA"}</span>
<span class="metric">LLM: ${data.used_llm}</span>
<span class="metric">graph: ${data.graph_used}</span>
<span class="metric">graph available: ${graph.graph_available}</span>
<span class="metric">fusion: ${fusion.fusion_used}</span>
<span class="metric">graph added: ${fusion.graph_added_count ?? "NA"}</span>
<span class="metric">graph supported: ${fusion.graph_supported_count ?? "NA"}</span>
`;
}
function updateCitations(data) {
const citations = data.citations || [];
const box = document.getElementById("citationsBox");
if (citations.length === 0) {
box.textContent = "No citations returned.";
return;
}
box.innerHTML = "";
citations.forEach((citation, index) => {
const card = document.createElement("div");
card.className = "citation-card";
card.innerHTML = `
<b>Source ${index + 1}</b><br>
<span>${citation.source_id || citation.id || "unknown source"}</span><br>
<span class="small">${citation.text_preview || citation.preview || ""}</span>
`;
box.appendChild(card);
});
}
async function sendMessage() {
const input = document.getElementById("messageInput");
const userText = input.value.trim();
const documentId = document.getElementById("documentId").value.trim();
if (!userText) return;
if (!documentId) {
alert("Paste document_id first.");
return;
}
const convo = getActiveConversation();
if (convo.title === "New chat") {
convo.title = userText.slice(0, 35);
}
convo.messages.push({
role: "user",
content: userText,
createdAt: new Date().toISOString()
});
input.value = "";
saveState();
renderConversations();
renderMessages();
setStatus("Thinking...");
const contextualQuery = buildContextualQuery(userText);
const payload = {
query: contextualQuery,
document_id: documentId,
top_k: 5,
retrieval_mode: "hybrid",
use_reranker: document.getElementById("useReranker").checked,
use_llm: document.getElementById("useLLM").checked,
use_graph: document.getElementById("useGraph").checked,
graph_entity_limit: 8,
use_graph_retrieval: document.getElementById("useGraphRetrieval").checked,
graph_retrieval_top_k: 5
};
try {
const response = await fetch("/ask", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
throw new Error(JSON.stringify(data));
}
const answer = data.answer || "I could not generate an answer.";
convo.messages.push({
role: "assistant",
content: answer,
createdAt: new Date().toISOString(),
raw: data
});
saveState();
renderMessages();
updateMetrics(data);
updateCitations(data);
setStatus("Ready");
} catch (error) {
convo.messages.push({
role: "assistant",
content: "Error: " + error.message,
createdAt: new Date().toISOString()
});
saveState();
renderMessages();
setStatus("Error");
}
}
async function buildGraph() {
const documentId = document.getElementById("documentId").value.trim();
if (!documentId) {
alert("Paste document_id first.");
return;
}
setStatus("Building graph...");
try {
const response = await fetch(`/documents/${documentId}/graph/build`, {
method: "POST"
});
const data = await response.json();
document.getElementById("metricsBox").innerHTML = `
<span class="metric">graph status: ${data.status || "done"}</span>
<span class="metric">entities: ${data.total_entities ?? data.entity_count ?? "NA"}</span>
<span class="metric">relations: ${data.total_relations ?? data.relation_count ?? "NA"}</span>
`;
setStatus("Graph built");
} catch (error) {
setStatus("Graph error");
alert("Graph build failed: " + error.message);
}
}
function openGraph() {
const documentId = document.getElementById("documentId").value.trim();
if (!documentId) {
alert("Paste document_id first.");
return;
}
window.open(`/documents/${documentId}/graph/view`, "_blank");
}
if (!activeConversationId || conversations.length === 0) {
newChat();
} else {
renderConversations();
renderMessages();
}
</script>
</body>
</html>
"""
'''
if "def get_product_app_html" not in text:
text += product_app_html
print("Added get_product_app_html.")
else:
print("get_product_app_html already exists.")
hf_path.write_text(text, encoding="utf-8")
# Patch main.py
main_path = Path("app/main.py")
main_text = main_path.read_text(encoding="utf-8-sig")
main_text = main_text.replace("\ufeff", "")
if "from app.deployment.hf_status import get_product_app_html" not in main_text:
main_text = "from app.deployment.hf_status import get_product_app_html\n" + main_text
# Ensure HTMLResponse import
if "HTMLResponse" not in main_text:
lines = main_text.splitlines()
inserted = False
for i, line in enumerate(lines):
if line.startswith("from fastapi.responses import"):
lines[i] = line + ", HTMLResponse"
inserted = True
break
if not inserted:
lines.insert(0, "from fastapi.responses import HTMLResponse")
main_text = "\n".join(lines) + "\n"
if "# Product ChatGPT-style app UI endpoint" not in main_text:
main_text += '''
# Product ChatGPT-style app UI endpoint
@app.get("/app", response_class=HTMLResponse)
def product_app_page():
return get_product_app_html()
'''
print("Added /app route.")
else:
print("/app route already exists.")
main_path.write_text(main_text, encoding="utf-8")
print("Phase 25 ChatGPT-style product app UI added.")