Spaces:
Sleeping
Sleeping
github-actions[bot] commited on
Commit ·
363fde4
1
Parent(s): 5b42e02
Sync from GitHub 0d5a3f5b657940732e673ece22eeb9f89cf08968
Browse files- frontend/app.py +332 -30
frontend/app.py
CHANGED
|
@@ -11,6 +11,20 @@ st.set_page_config(page_title="NotebookLM Clone", page_icon="📚", layout="wide
|
|
| 11 |
BACKEND_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
| 12 |
REQUEST_TIMEOUT_SECONDS = 30
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
def get_http_session() -> requests.Session:
|
| 16 |
session = st.session_state.get("http_session")
|
|
@@ -141,8 +155,186 @@ def remove_query_param(name: str) -> None:
|
|
| 141 |
st.experimental_set_query_params(**params)
|
| 142 |
|
| 143 |
|
| 144 |
-
|
| 145 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
if "page" not in st.session_state:
|
| 148 |
st.session_state["page"] = "Home"
|
|
@@ -166,7 +358,8 @@ if bridge_token and bridge_token != st.session_state["processed_bridge_token"]:
|
|
| 166 |
st.rerun()
|
| 167 |
|
| 168 |
with st.sidebar:
|
| 169 |
-
st.
|
|
|
|
| 170 |
st.text_input("Backend URL", value=BACKEND_URL, key="backend_url")
|
| 171 |
|
| 172 |
auth_ok, auth_result, _ = api_get("/auth/status")
|
|
@@ -181,7 +374,7 @@ with st.sidebar:
|
|
| 181 |
if login_ok:
|
| 182 |
st.rerun()
|
| 183 |
|
| 184 |
-
st.subheader("
|
| 185 |
st.caption(f"Mode: {auth_mode}")
|
| 186 |
bridge_error = st.session_state.get("bridge_error")
|
| 187 |
if bridge_error:
|
|
@@ -240,6 +433,25 @@ with st.sidebar:
|
|
| 240 |
st.session_state.pop("selected_thread_id", None)
|
| 241 |
st.rerun()
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
if not auth_ok:
|
| 244 |
st.error(f"Backend auth check failed: {auth_result}")
|
| 245 |
st.stop()
|
|
@@ -250,9 +462,20 @@ if not authenticated:
|
|
| 250 |
|
| 251 |
if page == "Home":
|
| 252 |
st.subheader("Home")
|
| 253 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
ok, result, _ = api_get("/health")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
if ok:
|
| 257 |
st.success("Backend health check passed.")
|
| 258 |
st.json(result)
|
|
@@ -262,10 +485,21 @@ if page == "Home":
|
|
| 262 |
|
| 263 |
elif page == "Notebooks":
|
| 264 |
st.subheader("Notebooks")
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
if submitted:
|
| 271 |
if notebook_title.strip():
|
|
@@ -282,15 +516,23 @@ elif page == "Notebooks":
|
|
| 282 |
else:
|
| 283 |
st.error("Notebook title is required.")
|
| 284 |
|
| 285 |
-
if st.button("Refresh notebooks"):
|
| 286 |
-
st.rerun()
|
| 287 |
-
|
| 288 |
ok, result = fetch_notebooks()
|
| 289 |
if ok:
|
| 290 |
notebooks = result if isinstance(result, list) else []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
if notebooks:
|
| 292 |
st.write("Your notebooks")
|
| 293 |
-
st.dataframe(notebooks, use_container_width=True)
|
| 294 |
|
| 295 |
notebook_options = {
|
| 296 |
f"{n['id']} - {n['title']}": n
|
|
@@ -298,7 +540,12 @@ elif page == "Notebooks":
|
|
| 298 |
if isinstance(n, dict) and "id" in n and "title" in n
|
| 299 |
}
|
| 300 |
labels = list(notebook_options.keys())
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
selected = notebook_options[selected_label]
|
| 303 |
if st.button("Open notebook"):
|
| 304 |
st.session_state["selected_notebook_id"] = selected["id"]
|
|
@@ -311,6 +558,7 @@ elif page == "Notebooks":
|
|
| 311 |
if selected_notebook_id:
|
| 312 |
st.divider()
|
| 313 |
st.subheader(f"Notebook: {selected_notebook_title}")
|
|
|
|
| 314 |
|
| 315 |
manage_left, manage_right = st.columns(2)
|
| 316 |
with manage_left:
|
|
@@ -359,9 +607,15 @@ elif page == "Notebooks":
|
|
| 359 |
st.error("Failed to delete notebook.")
|
| 360 |
st.code(str(delete_result))
|
| 361 |
|
| 362 |
-
source_tab, chat_tab, artifacts_tab = st.tabs(
|
|
|
|
|
|
|
| 363 |
|
| 364 |
with source_tab:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
ok, notebook_result = fetch_notebooks()
|
| 366 |
if not ok:
|
| 367 |
st.error("Failed to load notebooks.")
|
|
@@ -462,8 +716,23 @@ elif page == "Notebooks":
|
|
| 462 |
if ok:
|
| 463 |
sources = source_result if isinstance(source_result, list) else []
|
| 464 |
if sources:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
st.write("Sources")
|
| 466 |
-
st.dataframe(sources, use_container_width=True)
|
| 467 |
else:
|
| 468 |
st.info("No sources yet for this notebook.")
|
| 469 |
else:
|
|
@@ -471,11 +740,16 @@ elif page == "Notebooks":
|
|
| 471 |
st.code(str(source_result))
|
| 472 |
|
| 473 |
with chat_tab:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
st.write("Chat threads")
|
| 475 |
ok, thread_result, _ = api_get(
|
| 476 |
f"/notebooks/{selected_notebook_id}/threads",
|
| 477 |
)
|
| 478 |
threads = thread_result if (ok and isinstance(thread_result, list)) else []
|
|
|
|
| 479 |
|
| 480 |
with st.form("create_thread_form"):
|
| 481 |
thread_title = st.text_input("Thread title (optional)")
|
|
@@ -527,13 +801,12 @@ elif page == "Notebooks":
|
|
| 527 |
role = msg.get("role", "unknown")
|
| 528 |
content = msg.get("content", "")
|
| 529 |
citations = msg.get("citations", [])
|
| 530 |
-
if role == "assistant"
|
| 531 |
-
|
| 532 |
-
|
|
|
|
| 533 |
with st.expander("Citations", expanded=False):
|
| 534 |
-
st.dataframe(citations, use_container_width=True)
|
| 535 |
-
else:
|
| 536 |
-
st.markdown(f"**You:** {content}")
|
| 537 |
else:
|
| 538 |
st.error("Failed to fetch messages.")
|
| 539 |
st.code(str(message_result))
|
|
@@ -568,7 +841,10 @@ elif page == "Notebooks":
|
|
| 568 |
st.info("Create a thread to start chatting.")
|
| 569 |
|
| 570 |
with artifacts_tab:
|
| 571 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 572 |
|
| 573 |
report_col, quiz_col, podcast_col = st.columns(3)
|
| 574 |
|
|
@@ -700,9 +976,30 @@ elif page == "Notebooks":
|
|
| 700 |
value=bool(st.session_state.get(auto_refresh_key, True)),
|
| 701 |
key=auto_refresh_key,
|
| 702 |
)
|
| 703 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
artifact_options = {
|
| 705 |
-
|
| 706 |
for a in artifacts
|
| 707 |
if isinstance(a, dict) and "id" in a
|
| 708 |
}
|
|
@@ -716,6 +1013,16 @@ elif page == "Notebooks":
|
|
| 716 |
artifact_type = str(selected_artifact.get("type", ""))
|
| 717 |
artifact_status = str(selected_artifact.get("status", ""))
|
| 718 |
artifact_content = selected_artifact.get("content")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
|
| 720 |
if artifact_type == "report" and artifact_content:
|
| 721 |
st.markdown("### Report Preview")
|
|
@@ -764,11 +1071,6 @@ elif page == "Notebooks":
|
|
| 764 |
else:
|
| 765 |
st.info("Select an artifact to preview.")
|
| 766 |
|
| 767 |
-
in_flight = sum(
|
| 768 |
-
1
|
| 769 |
-
for a in artifacts
|
| 770 |
-
if str(a.get("status", "")).lower() in {"pending", "processing"}
|
| 771 |
-
)
|
| 772 |
if auto_refresh and in_flight > 0:
|
| 773 |
st.caption(
|
| 774 |
f"{in_flight} artifact(s) still processing. "
|
|
|
|
| 11 |
BACKEND_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
| 12 |
REQUEST_TIMEOUT_SECONDS = 30
|
| 13 |
|
| 14 |
+
STATUS_LABELS = {
|
| 15 |
+
"ready": "Ready",
|
| 16 |
+
"failed": "Failed",
|
| 17 |
+
"processing": "Processing",
|
| 18 |
+
"pending": "Pending",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
STATUS_ICONS = {
|
| 22 |
+
"ready": "✓",
|
| 23 |
+
"failed": "!",
|
| 24 |
+
"processing": "~",
|
| 25 |
+
"pending": "…",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
|
| 29 |
def get_http_session() -> requests.Session:
|
| 30 |
session = st.session_state.get("http_session")
|
|
|
|
| 155 |
st.experimental_set_query_params(**params)
|
| 156 |
|
| 157 |
|
| 158 |
+
def inject_theme() -> None:
|
| 159 |
+
st.markdown(
|
| 160 |
+
"""
|
| 161 |
+
<style>
|
| 162 |
+
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
|
| 163 |
+
|
| 164 |
+
:root {
|
| 165 |
+
--ink: #162525;
|
| 166 |
+
--ink-muted: #4f6362;
|
| 167 |
+
--card: #fefdf9;
|
| 168 |
+
--card-border: #d2dad3;
|
| 169 |
+
--accent: #146f67;
|
| 170 |
+
--accent-soft: #d6efeb;
|
| 171 |
+
--warn-soft: #fff2dd;
|
| 172 |
+
--warn-border: #f0bd69;
|
| 173 |
+
--ok-soft: #def5ec;
|
| 174 |
+
--ok-border: #53a27f;
|
| 175 |
+
--error-soft: #ffe6e3;
|
| 176 |
+
--error-border: #d2675a;
|
| 177 |
+
--pending-soft: #eceeef;
|
| 178 |
+
--pending-border: #9aa7ad;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.stApp {
|
| 182 |
+
background:
|
| 183 |
+
radial-gradient(circle at 15% -5%, #fff3de 0%, rgba(255, 243, 222, 0) 40%),
|
| 184 |
+
radial-gradient(circle at 88% 8%, #d9efeb 0%, rgba(217, 239, 235, 0) 42%),
|
| 185 |
+
linear-gradient(180deg, #f8faf8 0%, #f1f5f2 100%);
|
| 186 |
+
color: var(--ink);
|
| 187 |
+
font-family: "IBM Plex Sans", sans-serif;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
[data-testid="stSidebar"] {
|
| 191 |
+
background: linear-gradient(180deg, #173436 0%, #11292b 100%);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
[data-testid="stSidebar"] * {
|
| 195 |
+
color: #e9f1ef !important;
|
| 196 |
+
font-family: "IBM Plex Sans", sans-serif;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
h1, h2, h3, h4 {
|
| 200 |
+
font-family: "Space Grotesk", sans-serif;
|
| 201 |
+
letter-spacing: 0.02em;
|
| 202 |
+
color: var(--ink);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.app-hero {
|
| 206 |
+
background: linear-gradient(125deg, rgba(20, 111, 103, 0.13) 0%, rgba(255, 245, 228, 0.95) 56%, rgba(205, 231, 226, 0.9) 100%);
|
| 207 |
+
border: 1px solid #c9d9cf;
|
| 208 |
+
border-radius: 18px;
|
| 209 |
+
padding: 18px 22px;
|
| 210 |
+
margin-bottom: 14px;
|
| 211 |
+
box-shadow: 0 8px 24px rgba(17, 45, 42, 0.08);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.hero-kicker {
|
| 215 |
+
display: inline-block;
|
| 216 |
+
font-family: "Space Grotesk", sans-serif;
|
| 217 |
+
font-size: 0.75rem;
|
| 218 |
+
letter-spacing: 0.08em;
|
| 219 |
+
text-transform: uppercase;
|
| 220 |
+
color: #0f625b;
|
| 221 |
+
background: rgba(255, 255, 255, 0.7);
|
| 222 |
+
border: 1px solid rgba(15, 98, 91, 0.22);
|
| 223 |
+
border-radius: 999px;
|
| 224 |
+
padding: 3px 10px;
|
| 225 |
+
margin-bottom: 8px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.hero-title {
|
| 229 |
+
margin: 0;
|
| 230 |
+
font-family: "Space Grotesk", sans-serif;
|
| 231 |
+
font-size: clamp(1.45rem, 2.4vw, 2rem);
|
| 232 |
+
line-height: 1.2;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.hero-sub {
|
| 236 |
+
margin: 6px 0 0 0;
|
| 237 |
+
color: var(--ink-muted);
|
| 238 |
+
font-size: 0.95rem;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.soft-card {
|
| 242 |
+
background: rgba(255, 255, 255, 0.82);
|
| 243 |
+
border: 1px solid var(--card-border);
|
| 244 |
+
border-radius: 14px;
|
| 245 |
+
padding: 12px 14px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.metric-card {
|
| 249 |
+
background: var(--card);
|
| 250 |
+
border: 1px solid var(--card-border);
|
| 251 |
+
border-radius: 14px;
|
| 252 |
+
padding: 10px 12px;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.metric-label {
|
| 256 |
+
color: var(--ink-muted);
|
| 257 |
+
font-size: 0.78rem;
|
| 258 |
+
text-transform: uppercase;
|
| 259 |
+
letter-spacing: 0.06em;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.metric-value {
|
| 263 |
+
font-family: "Space Grotesk", sans-serif;
|
| 264 |
+
font-size: 1.4rem;
|
| 265 |
+
color: var(--ink);
|
| 266 |
+
margin-top: 2px;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.status-pill {
|
| 270 |
+
display: inline-block;
|
| 271 |
+
border-radius: 999px;
|
| 272 |
+
padding: 4px 10px;
|
| 273 |
+
font-size: 0.78rem;
|
| 274 |
+
font-weight: 600;
|
| 275 |
+
border: 1px solid transparent;
|
| 276 |
+
}
|
| 277 |
+
.status-ready {
|
| 278 |
+
background: var(--ok-soft);
|
| 279 |
+
border-color: var(--ok-border);
|
| 280 |
+
color: #1c6b4d;
|
| 281 |
+
}
|
| 282 |
+
.status-failed {
|
| 283 |
+
background: var(--error-soft);
|
| 284 |
+
border-color: var(--error-border);
|
| 285 |
+
color: #8d3329;
|
| 286 |
+
}
|
| 287 |
+
.status-processing {
|
| 288 |
+
background: var(--warn-soft);
|
| 289 |
+
border-color: var(--warn-border);
|
| 290 |
+
color: #7d581a;
|
| 291 |
+
}
|
| 292 |
+
.status-pending {
|
| 293 |
+
background: var(--pending-soft);
|
| 294 |
+
border-color: var(--pending-border);
|
| 295 |
+
color: #485a63;
|
| 296 |
+
}
|
| 297 |
+
</style>
|
| 298 |
+
""",
|
| 299 |
+
unsafe_allow_html=True,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def status_key(value: str | None) -> str:
|
| 304 |
+
return str(value or "").strip().lower()
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def render_status_pill(status: str | None, *, prefix: str = "") -> None:
|
| 308 |
+
key = status_key(status)
|
| 309 |
+
css = f"status-{key}" if key in STATUS_LABELS else "status-pending"
|
| 310 |
+
icon = STATUS_ICONS.get(key, "•")
|
| 311 |
+
label = STATUS_LABELS.get(key, str(status or "Unknown").title())
|
| 312 |
+
text = f"{prefix}{icon} {label}".strip()
|
| 313 |
+
st.markdown(f"<span class='status-pill {css}'>{text}</span>", unsafe_allow_html=True)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def render_metric_card(label: str, value: str | int) -> None:
|
| 317 |
+
st.markdown(
|
| 318 |
+
(
|
| 319 |
+
"<div class='metric-card'>"
|
| 320 |
+
f"<div class='metric-label'>{label}</div>"
|
| 321 |
+
f"<div class='metric-value'>{value}</div>"
|
| 322 |
+
"</div>"
|
| 323 |
+
),
|
| 324 |
+
unsafe_allow_html=True,
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def build_artifact_option_label(artifact: dict[str, Any]) -> str:
|
| 329 |
+
art_id = artifact.get("id", "?")
|
| 330 |
+
art_type = str(artifact.get("type", "artifact")).title()
|
| 331 |
+
status = status_key(str(artifact.get("status", "")))
|
| 332 |
+
icon = STATUS_ICONS.get(status, "•")
|
| 333 |
+
label = STATUS_LABELS.get(status, str(artifact.get("status", "unknown")).title())
|
| 334 |
+
return f"#{art_id} · {art_type} · {icon} {label}"
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
inject_theme()
|
| 338 |
|
| 339 |
if "page" not in st.session_state:
|
| 340 |
st.session_state["page"] = "Home"
|
|
|
|
| 358 |
st.rerun()
|
| 359 |
|
| 360 |
with st.sidebar:
|
| 361 |
+
st.markdown("### Workspace")
|
| 362 |
+
st.caption("Connected client settings")
|
| 363 |
st.text_input("Backend URL", value=BACKEND_URL, key="backend_url")
|
| 364 |
|
| 365 |
auth_ok, auth_result, _ = api_get("/auth/status")
|
|
|
|
| 374 |
if login_ok:
|
| 375 |
st.rerun()
|
| 376 |
|
| 377 |
+
st.subheader("Authentication")
|
| 378 |
st.caption(f"Mode: {auth_mode}")
|
| 379 |
bridge_error = st.session_state.get("bridge_error")
|
| 380 |
if bridge_error:
|
|
|
|
| 433 |
st.session_state.pop("selected_thread_id", None)
|
| 434 |
st.rerun()
|
| 435 |
|
| 436 |
+
hero_mode = auth_mode if isinstance(auth_mode, str) else "unknown"
|
| 437 |
+
hero_identity = "Not signed in"
|
| 438 |
+
if authenticated and isinstance(auth_user, dict):
|
| 439 |
+
hero_identity = str(auth_user.get("display_name") or auth_user.get("email") or "Signed in")
|
| 440 |
+
st.markdown(
|
| 441 |
+
(
|
| 442 |
+
"<div class='app-hero'>"
|
| 443 |
+
"<span class='hero-kicker'>Notebook Workspace</span>"
|
| 444 |
+
"<h1 class='hero-title'>NotebookLM Clone</h1>"
|
| 445 |
+
"<p class='hero-sub'>"
|
| 446 |
+
"Ingest sources, chat with citations, and generate reports, quizzes, and podcasts."
|
| 447 |
+
f" | Auth mode: <strong>{hero_mode}</strong>"
|
| 448 |
+
f" | User: <strong>{hero_identity}</strong>"
|
| 449 |
+
"</p>"
|
| 450 |
+
"</div>"
|
| 451 |
+
),
|
| 452 |
+
unsafe_allow_html=True,
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
if not auth_ok:
|
| 456 |
st.error(f"Backend auth check failed: {auth_result}")
|
| 457 |
st.stop()
|
|
|
|
| 462 |
|
| 463 |
if page == "Home":
|
| 464 |
st.subheader("Home")
|
| 465 |
+
st.markdown(
|
| 466 |
+
"<div class='soft-card'>Use this page to verify backend connectivity and auth status before working in notebooks.</div>",
|
| 467 |
+
unsafe_allow_html=True,
|
| 468 |
+
)
|
| 469 |
|
| 470 |
ok, result, _ = api_get("/health")
|
| 471 |
+
metric_col_1, metric_col_2, metric_col_3 = st.columns(3)
|
| 472 |
+
with metric_col_1:
|
| 473 |
+
render_metric_card("Auth Mode", hero_mode)
|
| 474 |
+
with metric_col_2:
|
| 475 |
+
render_metric_card("Session", "Signed in" if authenticated else "Signed out")
|
| 476 |
+
with metric_col_3:
|
| 477 |
+
render_metric_card("Backend", "Healthy" if ok else "Unavailable")
|
| 478 |
+
|
| 479 |
if ok:
|
| 480 |
st.success("Backend health check passed.")
|
| 481 |
st.json(result)
|
|
|
|
| 485 |
|
| 486 |
elif page == "Notebooks":
|
| 487 |
st.subheader("Notebooks")
|
| 488 |
+
st.markdown(
|
| 489 |
+
"<div class='soft-card'>Create or open a notebook, ingest sources, then use chat and artifact tools from one workspace.</div>",
|
| 490 |
+
unsafe_allow_html=True,
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
create_col, refresh_col = st.columns([3, 1])
|
| 494 |
+
with create_col:
|
| 495 |
+
with st.form("create_notebook_form"):
|
| 496 |
+
notebook_title = st.text_input("Notebook title", placeholder="e.g., AI Research Notes")
|
| 497 |
+
submitted = st.form_submit_button("Create notebook")
|
| 498 |
+
with refresh_col:
|
| 499 |
+
st.write("")
|
| 500 |
+
st.write("")
|
| 501 |
+
if st.button("Refresh notebooks", use_container_width=True):
|
| 502 |
+
st.rerun()
|
| 503 |
|
| 504 |
if submitted:
|
| 505 |
if notebook_title.strip():
|
|
|
|
| 516 |
else:
|
| 517 |
st.error("Notebook title is required.")
|
| 518 |
|
|
|
|
|
|
|
|
|
|
| 519 |
ok, result = fetch_notebooks()
|
| 520 |
if ok:
|
| 521 |
notebooks = result if isinstance(result, list) else []
|
| 522 |
+
selected_notebook_id = st.session_state.get("selected_notebook_id")
|
| 523 |
+
selected_notebook_title = st.session_state.get("selected_notebook_title")
|
| 524 |
+
|
| 525 |
+
metrics_left, metrics_mid, metrics_right = st.columns(3)
|
| 526 |
+
with metrics_left:
|
| 527 |
+
render_metric_card("Notebook Count", len(notebooks))
|
| 528 |
+
with metrics_mid:
|
| 529 |
+
render_metric_card("Selected Notebook", selected_notebook_id or "None")
|
| 530 |
+
with metrics_right:
|
| 531 |
+
render_metric_card("Workspace State", "Ready" if notebooks else "Empty")
|
| 532 |
+
|
| 533 |
if notebooks:
|
| 534 |
st.write("Your notebooks")
|
| 535 |
+
st.dataframe(notebooks, use_container_width=True, hide_index=True)
|
| 536 |
|
| 537 |
notebook_options = {
|
| 538 |
f"{n['id']} - {n['title']}": n
|
|
|
|
| 540 |
if isinstance(n, dict) and "id" in n and "title" in n
|
| 541 |
}
|
| 542 |
labels = list(notebook_options.keys())
|
| 543 |
+
default_index = 0
|
| 544 |
+
for idx, label in enumerate(labels):
|
| 545 |
+
if notebook_options[label]["id"] == selected_notebook_id:
|
| 546 |
+
default_index = idx
|
| 547 |
+
break
|
| 548 |
+
selected_label = st.selectbox("Select notebook to open", options=labels, index=default_index)
|
| 549 |
selected = notebook_options[selected_label]
|
| 550 |
if st.button("Open notebook"):
|
| 551 |
st.session_state["selected_notebook_id"] = selected["id"]
|
|
|
|
| 558 |
if selected_notebook_id:
|
| 559 |
st.divider()
|
| 560 |
st.subheader(f"Notebook: {selected_notebook_title}")
|
| 561 |
+
render_status_pill("ready", prefix="Notebook ")
|
| 562 |
|
| 563 |
manage_left, manage_right = st.columns(2)
|
| 564 |
with manage_left:
|
|
|
|
| 607 |
st.error("Failed to delete notebook.")
|
| 608 |
st.code(str(delete_result))
|
| 609 |
|
| 610 |
+
source_tab, chat_tab, artifacts_tab = st.tabs(
|
| 611 |
+
["Source Library", "Chat Workspace", "Artifact Studio"]
|
| 612 |
+
)
|
| 613 |
|
| 614 |
with source_tab:
|
| 615 |
+
st.markdown(
|
| 616 |
+
"<div class='soft-card'>Upload files or add links. Ingested sources are scoped to this notebook.</div>",
|
| 617 |
+
unsafe_allow_html=True,
|
| 618 |
+
)
|
| 619 |
ok, notebook_result = fetch_notebooks()
|
| 620 |
if not ok:
|
| 621 |
st.error("Failed to load notebooks.")
|
|
|
|
| 716 |
if ok:
|
| 717 |
sources = source_result if isinstance(source_result, list) else []
|
| 718 |
if sources:
|
| 719 |
+
ready_sources = sum(
|
| 720 |
+
1 for src in sources if status_key(str(src.get("status", ""))) == "ready"
|
| 721 |
+
)
|
| 722 |
+
processing_sources = sum(
|
| 723 |
+
1
|
| 724 |
+
for src in sources
|
| 725 |
+
if status_key(str(src.get("status", ""))) in {"processing", "pending"}
|
| 726 |
+
)
|
| 727 |
+
source_metrics_1, source_metrics_2, source_metrics_3 = st.columns(3)
|
| 728 |
+
with source_metrics_1:
|
| 729 |
+
render_metric_card("Source Count", len(sources))
|
| 730 |
+
with source_metrics_2:
|
| 731 |
+
render_metric_card("Ready Sources", ready_sources)
|
| 732 |
+
with source_metrics_3:
|
| 733 |
+
render_metric_card("In Flight", processing_sources)
|
| 734 |
st.write("Sources")
|
| 735 |
+
st.dataframe(sources, use_container_width=True, hide_index=True)
|
| 736 |
else:
|
| 737 |
st.info("No sources yet for this notebook.")
|
| 738 |
else:
|
|
|
|
| 740 |
st.code(str(source_result))
|
| 741 |
|
| 742 |
with chat_tab:
|
| 743 |
+
st.markdown(
|
| 744 |
+
"<div class='soft-card'>Ask questions grounded in your selected notebook sources. Citations are shown per assistant response.</div>",
|
| 745 |
+
unsafe_allow_html=True,
|
| 746 |
+
)
|
| 747 |
st.write("Chat threads")
|
| 748 |
ok, thread_result, _ = api_get(
|
| 749 |
f"/notebooks/{selected_notebook_id}/threads",
|
| 750 |
)
|
| 751 |
threads = thread_result if (ok and isinstance(thread_result, list)) else []
|
| 752 |
+
render_metric_card("Thread Count", len(threads))
|
| 753 |
|
| 754 |
with st.form("create_thread_form"):
|
| 755 |
thread_title = st.text_input("Thread title (optional)")
|
|
|
|
| 801 |
role = msg.get("role", "unknown")
|
| 802 |
content = msg.get("content", "")
|
| 803 |
citations = msg.get("citations", [])
|
| 804 |
+
message_type = "assistant" if role == "assistant" else "user"
|
| 805 |
+
with st.chat_message(message_type):
|
| 806 |
+
st.markdown(str(content))
|
| 807 |
+
if role == "assistant" and isinstance(citations, list) and citations:
|
| 808 |
with st.expander("Citations", expanded=False):
|
| 809 |
+
st.dataframe(citations, use_container_width=True, hide_index=True)
|
|
|
|
|
|
|
| 810 |
else:
|
| 811 |
st.error("Failed to fetch messages.")
|
| 812 |
st.code(str(message_result))
|
|
|
|
| 841 |
st.info("Create a thread to start chatting.")
|
| 842 |
|
| 843 |
with artifacts_tab:
|
| 844 |
+
st.markdown(
|
| 845 |
+
"<div class='soft-card'>Generate structured outputs from notebook context and track progress here.</div>",
|
| 846 |
+
unsafe_allow_html=True,
|
| 847 |
+
)
|
| 848 |
|
| 849 |
report_col, quiz_col, podcast_col = st.columns(3)
|
| 850 |
|
|
|
|
| 976 |
value=bool(st.session_state.get(auto_refresh_key, True)),
|
| 977 |
key=auto_refresh_key,
|
| 978 |
)
|
| 979 |
+
ready_count = sum(
|
| 980 |
+
1 for artifact in artifacts if status_key(str(artifact.get("status", ""))) == "ready"
|
| 981 |
+
)
|
| 982 |
+
failed_count = sum(
|
| 983 |
+
1 for artifact in artifacts if status_key(str(artifact.get("status", ""))) == "failed"
|
| 984 |
+
)
|
| 985 |
+
in_flight = sum(
|
| 986 |
+
1
|
| 987 |
+
for artifact in artifacts
|
| 988 |
+
if status_key(str(artifact.get("status", ""))) in {"pending", "processing"}
|
| 989 |
+
)
|
| 990 |
+
artifact_metric_1, artifact_metric_2, artifact_metric_3, artifact_metric_4 = st.columns(4)
|
| 991 |
+
with artifact_metric_1:
|
| 992 |
+
render_metric_card("Artifacts", len(artifacts))
|
| 993 |
+
with artifact_metric_2:
|
| 994 |
+
render_metric_card("Ready", ready_count)
|
| 995 |
+
with artifact_metric_3:
|
| 996 |
+
render_metric_card("Processing", in_flight)
|
| 997 |
+
with artifact_metric_4:
|
| 998 |
+
render_metric_card("Failed", failed_count)
|
| 999 |
+
|
| 1000 |
+
st.dataframe(artifacts, use_container_width=True, hide_index=True)
|
| 1001 |
artifact_options = {
|
| 1002 |
+
build_artifact_option_label(a): a
|
| 1003 |
for a in artifacts
|
| 1004 |
if isinstance(a, dict) and "id" in a
|
| 1005 |
}
|
|
|
|
| 1013 |
artifact_type = str(selected_artifact.get("type", ""))
|
| 1014 |
artifact_status = str(selected_artifact.get("status", ""))
|
| 1015 |
artifact_content = selected_artifact.get("content")
|
| 1016 |
+
artifact_error = selected_artifact.get("error_message")
|
| 1017 |
+
|
| 1018 |
+
status_col, type_col = st.columns([1, 4])
|
| 1019 |
+
with status_col:
|
| 1020 |
+
render_status_pill(artifact_status)
|
| 1021 |
+
with type_col:
|
| 1022 |
+
st.caption(f"Artifact type: {artifact_type}")
|
| 1023 |
+
|
| 1024 |
+
if artifact_error:
|
| 1025 |
+
st.error(str(artifact_error))
|
| 1026 |
|
| 1027 |
if artifact_type == "report" and artifact_content:
|
| 1028 |
st.markdown("### Report Preview")
|
|
|
|
| 1071 |
else:
|
| 1072 |
st.info("Select an artifact to preview.")
|
| 1073 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
if auto_refresh and in_flight > 0:
|
| 1075 |
st.caption(
|
| 1076 |
f"{in_flight} artifact(s) still processing. "
|