Spaces:
Sleeping
Sleeping
internomega-terrablue commited on
Commit ·
e90d887
1
Parent(s): 4bc41f5
change to gradio
Browse files- Dockerfile +0 -21
- README.md +9 -7
- app.py +460 -707
- auth.py +0 -60
- entrypoint.sh +0 -26
- ui/artifact_page.py → mock_data.py +72 -491
- {ui → pages}/__init__.py +0 -0
- pages/artifacts.py +238 -0
- pages/chat.py +91 -0
- pages/sources.py +172 -0
- requirements.txt +1 -2
- state.py +125 -0
- theme.py +333 -0
- ui/chat_page.py +0 -165
- ui/upload_page.py +0 -199
Dockerfile
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
FROM python:3.12-slim
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
RUN apt-get update && apt-get install -y \
|
| 6 |
-
build-essential \
|
| 7 |
-
curl \
|
| 8 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
-
|
| 10 |
-
COPY requirements.txt ./
|
| 11 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
-
|
| 13 |
-
COPY . .
|
| 14 |
-
|
| 15 |
-
RUN chmod +x /app/entrypoint.sh
|
| 16 |
-
|
| 17 |
-
EXPOSE 8501
|
| 18 |
-
|
| 19 |
-
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 20 |
-
|
| 21 |
-
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
| 1 |
---
|
| 2 |
title: NotebookLM
|
| 3 |
emoji: 🚀
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
hf_oauth: true
|
| 9 |
hf_oauth_expiration_minutes: 480
|
| 10 |
tags:
|
| 11 |
-
-
|
| 12 |
pinned: false
|
| 13 |
-
short_description:
|
| 14 |
license: mit
|
| 15 |
---
|
| 16 |
|
| 17 |
# NotebookLM Clone
|
| 18 |
|
| 19 |
-
AI-powered study companion built with
|
|
|
|
| 1 |
---
|
| 2 |
title: NotebookLM
|
| 3 |
emoji: 🚀
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "5.12.0"
|
| 8 |
+
python_version: "3.12"
|
| 9 |
+
app_file: app.py
|
| 10 |
hf_oauth: true
|
| 11 |
hf_oauth_expiration_minutes: 480
|
| 12 |
tags:
|
| 13 |
+
- gradio
|
| 14 |
pinned: false
|
| 15 |
+
short_description: NotebookLM - AI-Powered Study Companion
|
| 16 |
license: mit
|
| 17 |
---
|
| 18 |
|
| 19 |
# NotebookLM Clone
|
| 20 |
|
| 21 |
+
AI-powered study companion built with Gradio on Hugging Face Spaces.
|
app.py
CHANGED
|
@@ -1,721 +1,474 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
import
|
| 4 |
-
|
| 5 |
-
from
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
<stop offset="100%" style="stop-color:#764ba2"/>
|
| 14 |
-
</linearGradient>
|
| 15 |
-
<linearGradient id="lg2" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 16 |
-
<stop offset="0%" style="stop-color:#a78bfa"/>
|
| 17 |
-
<stop offset="100%" style="stop-color:#667eea"/>
|
| 18 |
-
</linearGradient>
|
| 19 |
-
<linearGradient id="sp" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 20 |
-
<stop offset="0%" style="stop-color:#fbbf24"/>
|
| 21 |
-
<stop offset="100%" style="stop-color:#f59e0b"/>
|
| 22 |
-
</linearGradient>
|
| 23 |
-
</defs>
|
| 24 |
-
<g transform="translate(4,6)">
|
| 25 |
-
<rect x="2" y="4" width="36" height="44" rx="4" fill="url(#lg1)"/>
|
| 26 |
-
<rect x="2" y="4" width="8" height="44" rx="3" fill="url(#lg2)" opacity="0.7"/>
|
| 27 |
-
<line x1="16" y1="16" x2="32" y2="16" stroke="rgba(255,255,255,0.5)" stroke-width="1.8" stroke-linecap="round"/>
|
| 28 |
-
<line x1="16" y1="23" x2="30" y2="23" stroke="rgba(255,255,255,0.4)" stroke-width="1.8" stroke-linecap="round"/>
|
| 29 |
-
<line x1="16" y1="30" x2="32" y2="30" stroke="rgba(255,255,255,0.5)" stroke-width="1.8" stroke-linecap="round"/>
|
| 30 |
-
<line x1="16" y1="37" x2="28" y2="37" stroke="rgba(255,255,255,0.4)" stroke-width="1.8" stroke-linecap="round"/>
|
| 31 |
-
<g transform="translate(32,2)">
|
| 32 |
-
<path d="M6 0 L7.5 4.5 L12 6 L7.5 7.5 L6 12 L4.5 7.5 L0 6 L4.5 4.5 Z" fill="url(#sp)"/>
|
| 33 |
-
<path d="M14 8 L14.8 10.2 L17 11 L14.8 11.8 L14 14 L13.2 11.8 L11 11 L13.2 10.2 Z" fill="#fbbf24" opacity="0.7"/>
|
| 34 |
-
</g>
|
| 35 |
-
</g>
|
| 36 |
-
<text x="56" y="28" font-family="Inter,-apple-system,sans-serif" font-size="22" font-weight="700">
|
| 37 |
-
<tspan fill="url(#lg1)">Notebook</tspan><tspan fill="#a78bfa" font-weight="800">LM</tspan>
|
| 38 |
-
</text>
|
| 39 |
-
<text x="57" y="46" font-family="Inter,-apple-system,sans-serif" font-size="10.5" fill="#8888aa" font-weight="400" letter-spacing="0.8">
|
| 40 |
-
AI-Powered Study Companion
|
| 41 |
-
</text>
|
| 42 |
-
</svg>"""
|
| 43 |
-
|
| 44 |
-
# Small icon-only version for headers
|
| 45 |
-
LOGO_ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 60">
|
| 46 |
-
<defs>
|
| 47 |
-
<linearGradient id="ig1" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 48 |
-
<stop offset="0%" style="stop-color:#667eea"/>
|
| 49 |
-
<stop offset="100%" style="stop-color:#764ba2"/>
|
| 50 |
-
</linearGradient>
|
| 51 |
-
<linearGradient id="ig2" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 52 |
-
<stop offset="0%" style="stop-color:#a78bfa"/>
|
| 53 |
-
<stop offset="100%" style="stop-color:#667eea"/>
|
| 54 |
-
</linearGradient>
|
| 55 |
-
<linearGradient id="isp" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 56 |
-
<stop offset="0%" style="stop-color:#fbbf24"/>
|
| 57 |
-
<stop offset="100%" style="stop-color:#f59e0b"/>
|
| 58 |
-
</linearGradient>
|
| 59 |
-
</defs>
|
| 60 |
-
<g transform="translate(2,4)">
|
| 61 |
-
<rect x="2" y="4" width="36" height="44" rx="5" fill="url(#ig1)"/>
|
| 62 |
-
<rect x="2" y="4" width="9" height="44" rx="4" fill="url(#ig2)" opacity="0.7"/>
|
| 63 |
-
<line x1="16" y1="16" x2="32" y2="16" stroke="rgba(255,255,255,0.55)" stroke-width="2" stroke-linecap="round"/>
|
| 64 |
-
<line x1="16" y1="23" x2="30" y2="23" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round"/>
|
| 65 |
-
<line x1="16" y1="30" x2="32" y2="30" stroke="rgba(255,255,255,0.55)" stroke-width="2" stroke-linecap="round"/>
|
| 66 |
-
<line x1="16" y1="37" x2="28" y2="37" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round"/>
|
| 67 |
-
<g transform="translate(30,0)">
|
| 68 |
-
<path d="M7 0 L8.8 5.3 L14 7 L8.8 8.8 L7 14 L5.2 8.8 L0 7 L5.2 5.3 Z" fill="url(#isp)"/>
|
| 69 |
-
<path d="M16 9 L17 11.5 L19.5 12.5 L17 13.5 L16 16 L15 13.5 L12.5 12.5 L15 11.5 Z" fill="#fbbf24" opacity="0.7"/>
|
| 70 |
-
</g>
|
| 71 |
-
</g>
|
| 72 |
-
</svg>"""
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def get_logo_b64(svg_str: str) -> str:
|
| 76 |
-
return base64.b64encode(svg_str.encode()).decode()
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
LOGO_B64 = get_logo_b64(LOGO_SVG)
|
| 80 |
-
ICON_B64 = get_logo_b64(LOGO_ICON_SVG)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# ── Page Config ──────────────────────────────────────────────────────────────
|
| 84 |
-
st.set_page_config(
|
| 85 |
-
page_title="NotebookLM",
|
| 86 |
-
page_icon="📓",
|
| 87 |
-
layout="wide",
|
| 88 |
-
initial_sidebar_state="expanded",
|
| 89 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
# ──
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
/
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
border-right: 1px solid rgba(255,255,255,0.06);
|
| 106 |
-
}
|
| 107 |
-
section[data-testid="stSidebar"] .stMarkdown h1 {
|
| 108 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 109 |
-
-webkit-background-clip: text;
|
| 110 |
-
-webkit-text-fill-color: transparent;
|
| 111 |
-
font-weight: 700;
|
| 112 |
-
font-size: 1.8rem;
|
| 113 |
-
letter-spacing: -0.5px;
|
| 114 |
-
}
|
| 115 |
-
section[data-testid="stSidebar"] p,
|
| 116 |
-
section[data-testid="stSidebar"] span,
|
| 117 |
-
section[data-testid="stSidebar"] label {
|
| 118 |
-
color: #c0c0d0 !important;
|
| 119 |
-
}
|
| 120 |
-
section[data-testid="stSidebar"] .stDivider {
|
| 121 |
-
border-color: rgba(255,255,255,0.08) !important;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
/* ── Notebook button in sidebar ── */
|
| 125 |
-
section[data-testid="stSidebar"] .stButton > button {
|
| 126 |
-
border-radius: 10px !important;
|
| 127 |
-
font-size: 0.85rem !important;
|
| 128 |
-
font-weight: 500 !important;
|
| 129 |
-
padding: 8px 14px !important;
|
| 130 |
-
transition: all 0.2s ease !important;
|
| 131 |
-
}
|
| 132 |
-
section[data-testid="stSidebar"] .stButton > button[kind="primary"] {
|
| 133 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 134 |
-
border: none !important;
|
| 135 |
-
color: white !important;
|
| 136 |
-
}
|
| 137 |
-
section[data-testid="stSidebar"] .stButton > button[kind="secondary"] {
|
| 138 |
-
background: rgba(255,255,255,0.05) !important;
|
| 139 |
-
border: 1px solid rgba(255,255,255,0.1) !important;
|
| 140 |
-
color: #d0d0e0 !important;
|
| 141 |
-
}
|
| 142 |
-
section[data-testid="stSidebar"] .stButton > button[kind="secondary"]:hover {
|
| 143 |
-
background: rgba(255,255,255,0.1) !important;
|
| 144 |
-
border-color: rgba(102,126,234,0.4) !important;
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
/* ── Tab styling ── */
|
| 148 |
-
.stTabs [data-baseweb="tab-list"] {
|
| 149 |
-
gap: 0px;
|
| 150 |
-
background: rgba(255,255,255,0.03);
|
| 151 |
-
border-radius: 12px;
|
| 152 |
-
padding: 4px;
|
| 153 |
-
border: 1px solid rgba(255,255,255,0.06);
|
| 154 |
-
}
|
| 155 |
-
.stTabs [data-baseweb="tab"] {
|
| 156 |
-
border-radius: 10px;
|
| 157 |
-
padding: 10px 24px;
|
| 158 |
-
font-weight: 500;
|
| 159 |
-
font-size: 0.9rem;
|
| 160 |
-
color: #888;
|
| 161 |
-
}
|
| 162 |
-
.stTabs [aria-selected="true"] {
|
| 163 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 164 |
-
color: white !important;
|
| 165 |
-
font-weight: 600;
|
| 166 |
-
}
|
| 167 |
-
.stTabs [data-baseweb="tab-highlight"] {
|
| 168 |
-
display: none;
|
| 169 |
-
}
|
| 170 |
-
.stTabs [data-baseweb="tab-border"] {
|
| 171 |
-
display: none;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
/* ── Main container ── */
|
| 175 |
-
.main .block-container {
|
| 176 |
-
padding-top: 2rem;
|
| 177 |
-
max-width: 1100px;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
/* ── Cards ── */
|
| 181 |
-
div[data-testid="stVerticalBlock"] > div[data-testid="stContainer"] {
|
| 182 |
-
border-radius: 14px !important;
|
| 183 |
-
border: 1px solid rgba(255,255,255,0.08) !important;
|
| 184 |
-
background: rgba(255,255,255,0.02) !important;
|
| 185 |
-
transition: all 0.2s ease;
|
| 186 |
-
}
|
| 187 |
-
div[data-testid="stVerticalBlock"] > div[data-testid="stContainer"]:hover {
|
| 188 |
-
border-color: rgba(102,126,234,0.3) !important;
|
| 189 |
-
background: rgba(255,255,255,0.04) !important;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
/* ── Chat messages ── */
|
| 193 |
-
.stChatMessage {
|
| 194 |
-
border-radius: 14px !important;
|
| 195 |
-
padding: 16px 20px !important;
|
| 196 |
-
margin-bottom: 8px !important;
|
| 197 |
-
}
|
| 198 |
-
div[data-testid="stChatMessageAvatarUser"] {
|
| 199 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 200 |
-
}
|
| 201 |
-
div[data-testid="stChatMessageAvatarAssistant"] {
|
| 202 |
-
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
/* ── Chat input ── */
|
| 206 |
-
.stChatInput > div {
|
| 207 |
-
border-radius: 14px !important;
|
| 208 |
-
border: 1px solid rgba(255,255,255,0.1) !important;
|
| 209 |
-
background: rgba(255,255,255,0.03) !important;
|
| 210 |
-
}
|
| 211 |
-
.stChatInput > div:focus-within {
|
| 212 |
-
border-color: #667eea !important;
|
| 213 |
-
box-shadow: 0 0 0 2px rgba(102,126,234,0.2) !important;
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
/* ── Expanders ── */
|
| 217 |
-
.streamlit-expanderHeader {
|
| 218 |
-
border-radius: 10px !important;
|
| 219 |
-
font-weight: 500 !important;
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
/* ── File uploader ── */
|
| 223 |
-
section[data-testid="stFileUploader"] > div {
|
| 224 |
-
border-radius: 14px !important;
|
| 225 |
-
border: 2px dashed rgba(102,126,234,0.3) !important;
|
| 226 |
-
background: rgba(102,126,234,0.03) !important;
|
| 227 |
-
}
|
| 228 |
-
section[data-testid="stFileUploader"] > div:hover {
|
| 229 |
-
border-color: rgba(102,126,234,0.6) !important;
|
| 230 |
-
background: rgba(102,126,234,0.06) !important;
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
/* ── Main content buttons ── */
|
| 234 |
-
.main .stButton > button[kind="primary"] {
|
| 235 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 236 |
-
border: none !important;
|
| 237 |
-
border-radius: 10px !important;
|
| 238 |
-
font-weight: 600 !important;
|
| 239 |
-
padding: 8px 20px !important;
|
| 240 |
-
transition: all 0.2s ease !important;
|
| 241 |
-
}
|
| 242 |
-
.main .stButton > button[kind="primary"]:hover {
|
| 243 |
-
opacity: 0.9 !important;
|
| 244 |
-
transform: translateY(-1px) !important;
|
| 245 |
-
box-shadow: 0 4px 15px rgba(102,126,234,0.3) !important;
|
| 246 |
-
}
|
| 247 |
-
.main .stButton > button[kind="secondary"] {
|
| 248 |
-
border-radius: 10px !important;
|
| 249 |
-
border: 1px solid rgba(255,255,255,0.12) !important;
|
| 250 |
-
background: rgba(255,255,255,0.04) !important;
|
| 251 |
-
font-weight: 500 !important;
|
| 252 |
-
transition: all 0.2s ease !important;
|
| 253 |
-
}
|
| 254 |
-
.main .stButton > button[kind="secondary"]:hover {
|
| 255 |
-
background: rgba(255,255,255,0.08) !important;
|
| 256 |
-
border-color: rgba(255,255,255,0.2) !important;
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
/* ── Download button ── */
|
| 260 |
-
.stDownloadButton > button {
|
| 261 |
-
border-radius: 10px !important;
|
| 262 |
-
font-weight: 500 !important;
|
| 263 |
-
}
|
| 264 |
-
|
| 265 |
-
/* ── Text input ── */
|
| 266 |
-
.stTextInput > div > div {
|
| 267 |
-
border-radius: 10px !important;
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
/* ── Metrics / status badges ── */
|
| 271 |
-
.stSuccess, .stWarning, .stError, .stInfo {
|
| 272 |
-
border-radius: 10px !important;
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
/* ── Welcome hero ── */
|
| 276 |
-
.welcome-hero {
|
| 277 |
-
text-align: center;
|
| 278 |
-
padding: 80px 40px;
|
| 279 |
-
background: linear-gradient(135deg, rgba(102,126,234,0.08) 0%, rgba(118,75,162,0.08) 100%);
|
| 280 |
-
border-radius: 20px;
|
| 281 |
-
border: 1px solid rgba(102,126,234,0.15);
|
| 282 |
-
margin: 20px 0;
|
| 283 |
-
}
|
| 284 |
-
.welcome-hero h1 {
|
| 285 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 286 |
-
-webkit-background-clip: text;
|
| 287 |
-
-webkit-text-fill-color: transparent;
|
| 288 |
-
font-size: 2.5rem;
|
| 289 |
-
font-weight: 700;
|
| 290 |
-
margin-bottom: 12px;
|
| 291 |
-
}
|
| 292 |
-
.welcome-hero p {
|
| 293 |
-
color: #9090a8;
|
| 294 |
-
font-size: 1.1rem;
|
| 295 |
-
line-height: 1.6;
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
/* ── Empty state ── */
|
| 299 |
-
.empty-state {
|
| 300 |
-
text-align: center;
|
| 301 |
-
padding: 60px 30px;
|
| 302 |
-
color: #707088;
|
| 303 |
-
}
|
| 304 |
-
.empty-state h3 {
|
| 305 |
-
color: #a0a0b8;
|
| 306 |
-
margin-bottom: 8px;
|
| 307 |
-
font-weight: 600;
|
| 308 |
-
}
|
| 309 |
-
.empty-state p {
|
| 310 |
-
font-size: 0.95rem;
|
| 311 |
-
line-height: 1.5;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
/* ── Source card ── */
|
| 315 |
-
.source-card {
|
| 316 |
-
display: flex;
|
| 317 |
-
align-items: center;
|
| 318 |
-
gap: 16px;
|
| 319 |
-
padding: 16px 20px;
|
| 320 |
-
background: rgba(255,255,255,0.02);
|
| 321 |
-
border: 1px solid rgba(255,255,255,0.08);
|
| 322 |
-
border-radius: 14px;
|
| 323 |
-
margin-bottom: 10px;
|
| 324 |
-
transition: all 0.2s ease;
|
| 325 |
-
}
|
| 326 |
-
.source-card:hover {
|
| 327 |
-
border-color: rgba(102,126,234,0.3);
|
| 328 |
-
background: rgba(255,255,255,0.04);
|
| 329 |
-
}
|
| 330 |
-
.source-icon {
|
| 331 |
-
width: 48px;
|
| 332 |
-
height: 48px;
|
| 333 |
-
border-radius: 12px;
|
| 334 |
-
display: flex;
|
| 335 |
-
align-items: center;
|
| 336 |
-
justify-content: center;
|
| 337 |
-
font-size: 1.5rem;
|
| 338 |
-
flex-shrink: 0;
|
| 339 |
-
}
|
| 340 |
-
.source-icon.pdf { background: rgba(239,68,68,0.15); }
|
| 341 |
-
.source-icon.pptx { background: rgba(249,115,22,0.15); }
|
| 342 |
-
.source-icon.txt { background: rgba(59,130,246,0.15); }
|
| 343 |
-
.source-icon.url { background: rgba(34,197,94,0.15); }
|
| 344 |
-
.source-icon.youtube { background: rgba(239,68,68,0.15); }
|
| 345 |
-
.source-info { flex: 1; min-width: 0; }
|
| 346 |
-
.source-info .name {
|
| 347 |
-
font-weight: 600;
|
| 348 |
-
font-size: 0.95rem;
|
| 349 |
-
color: #e0e0f0;
|
| 350 |
-
white-space: nowrap;
|
| 351 |
-
overflow: hidden;
|
| 352 |
-
text-overflow: ellipsis;
|
| 353 |
-
}
|
| 354 |
-
.source-info .meta {
|
| 355 |
-
font-size: 0.8rem;
|
| 356 |
-
color: #707088;
|
| 357 |
-
margin-top: 2px;
|
| 358 |
-
}
|
| 359 |
-
.source-badge {
|
| 360 |
-
padding: 4px 12px;
|
| 361 |
-
border-radius: 20px;
|
| 362 |
-
font-size: 0.75rem;
|
| 363 |
-
font-weight: 600;
|
| 364 |
-
letter-spacing: 0.3px;
|
| 365 |
-
}
|
| 366 |
-
.source-badge.ready {
|
| 367 |
-
background: rgba(34,197,94,0.15);
|
| 368 |
-
color: #22c55e;
|
| 369 |
-
}
|
| 370 |
-
.source-badge.processing {
|
| 371 |
-
background: rgba(234,179,8,0.15);
|
| 372 |
-
color: #eab308;
|
| 373 |
-
}
|
| 374 |
-
.source-badge.failed {
|
| 375 |
-
background: rgba(239,68,68,0.15);
|
| 376 |
-
color: #ef4444;
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
/* ── Artifact generation cards ── */
|
| 380 |
-
.gen-card {
|
| 381 |
-
text-align: center;
|
| 382 |
-
padding: 30px 20px;
|
| 383 |
-
background: rgba(255,255,255,0.02);
|
| 384 |
-
border: 1px solid rgba(255,255,255,0.08);
|
| 385 |
-
border-radius: 16px;
|
| 386 |
-
transition: all 0.25s ease;
|
| 387 |
-
cursor: default;
|
| 388 |
-
}
|
| 389 |
-
.gen-card:hover {
|
| 390 |
-
border-color: rgba(102,126,234,0.3);
|
| 391 |
-
background: rgba(102,126,234,0.04);
|
| 392 |
-
transform: translateY(-2px);
|
| 393 |
-
}
|
| 394 |
-
.gen-card .icon {
|
| 395 |
-
font-size: 2.5rem;
|
| 396 |
-
margin-bottom: 12px;
|
| 397 |
-
}
|
| 398 |
-
.gen-card h4 {
|
| 399 |
-
margin: 0 0 6px 0;
|
| 400 |
-
font-weight: 600;
|
| 401 |
-
color: #e0e0f0;
|
| 402 |
-
}
|
| 403 |
-
.gen-card p {
|
| 404 |
-
font-size: 0.85rem;
|
| 405 |
-
color: #808098;
|
| 406 |
-
line-height: 1.4;
|
| 407 |
-
margin: 0;
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
/* ── Citation chip ── */
|
| 411 |
-
.citation-chip {
|
| 412 |
-
display: inline-flex;
|
| 413 |
-
align-items: center;
|
| 414 |
-
gap: 6px;
|
| 415 |
-
padding: 6px 14px;
|
| 416 |
-
background: rgba(102,126,234,0.1);
|
| 417 |
-
border: 1px solid rgba(102,126,234,0.2);
|
| 418 |
-
border-radius: 20px;
|
| 419 |
-
font-size: 0.8rem;
|
| 420 |
-
color: #a0b0f0;
|
| 421 |
-
margin: 3px 4px;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
/* ── Notebook header ── */
|
| 425 |
-
.notebook-header {
|
| 426 |
-
padding: 0 0 16px 0;
|
| 427 |
-
margin-bottom: 16px;
|
| 428 |
-
border-bottom: 1px solid rgba(255,255,255,0.06);
|
| 429 |
-
}
|
| 430 |
-
.notebook-header h2 {
|
| 431 |
-
font-weight: 700;
|
| 432 |
-
font-size: 1.5rem;
|
| 433 |
-
margin: 0;
|
| 434 |
-
color: #e8e8f8;
|
| 435 |
-
}
|
| 436 |
-
.notebook-header .meta {
|
| 437 |
-
font-size: 0.85rem;
|
| 438 |
-
color: #707088;
|
| 439 |
-
margin-top: 4px;
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
/* ── Hide ALL Streamlit default chrome ── */
|
| 443 |
-
[data-testid="stSidebarCollapseButton"],
|
| 444 |
-
[data-testid="collapsedControl"],
|
| 445 |
-
.stDeployButton,
|
| 446 |
-
[data-testid="stToolbar"],
|
| 447 |
-
[data-testid="stBottomBlockContainer"],
|
| 448 |
-
[data-testid="manage-app-button"] {
|
| 449 |
-
display: none !important;
|
| 450 |
-
}
|
| 451 |
-
/* Kill all Material Icon text leaks */
|
| 452 |
-
[data-testid="stSidebarCollapseButton"] *,
|
| 453 |
-
[data-testid="collapsedControl"] *,
|
| 454 |
-
[data-testid="stBottomBlockContainer"] * {
|
| 455 |
-
display: none !important;
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
-
/* ── Force dark theme on main area ── */
|
| 459 |
-
.stApp, .stApp > header {
|
| 460 |
-
background-color: #0e1117 !important;
|
| 461 |
-
}
|
| 462 |
-
.main .block-container {
|
| 463 |
-
background-color: #0e1117 !important;
|
| 464 |
-
}
|
| 465 |
-
.stApp [data-testid="stHeader"] {
|
| 466 |
-
background-color: #0e1117 !important;
|
| 467 |
-
}
|
| 468 |
-
/* All text defaults to light */
|
| 469 |
-
.stApp, .stApp p, .stApp span, .stApp li, .stApp td, .stApp th {
|
| 470 |
-
color: #c8c8d8 !important;
|
| 471 |
-
}
|
| 472 |
-
.stApp h1, .stApp h2, .stApp h3, .stApp h4 {
|
| 473 |
-
color: #e0e0f0 !important;
|
| 474 |
-
}
|
| 475 |
-
.stApp strong {
|
| 476 |
-
color: #e8e8f8 !important;
|
| 477 |
-
}
|
| 478 |
-
/* Markdown inside containers */
|
| 479 |
-
.stMarkdown, .stMarkdown p {
|
| 480 |
-
color: #c8c8d8 !important;
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
/* ── Expander styling for dark ── */
|
| 484 |
-
.streamlit-expanderHeader {
|
| 485 |
-
background: rgba(255,255,255,0.03) !important;
|
| 486 |
-
border-radius: 10px !important;
|
| 487 |
-
color: #c0c0d0 !important;
|
| 488 |
-
}
|
| 489 |
-
.streamlit-expanderContent {
|
| 490 |
-
background: rgba(255,255,255,0.01) !important;
|
| 491 |
-
border-color: rgba(255,255,255,0.06) !important;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
/* ── Table styling ── */
|
| 495 |
-
.stApp table {
|
| 496 |
-
border-collapse: collapse;
|
| 497 |
-
}
|
| 498 |
-
.stApp th {
|
| 499 |
-
background: rgba(102,126,234,0.1) !important;
|
| 500 |
-
border-bottom: 1px solid rgba(255,255,255,0.1) !important;
|
| 501 |
-
}
|
| 502 |
-
.stApp td {
|
| 503 |
-
border-bottom: 1px solid rgba(255,255,255,0.05) !important;
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
/* ── Radio / select styling ── */
|
| 507 |
-
.stRadio label, .stSelectbox label, .stSlider label {
|
| 508 |
-
color: #a0a0b8 !important;
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
/* ── Alert boxes ── */
|
| 512 |
-
[data-testid="stAlert"] {
|
| 513 |
-
background: rgba(255,255,255,0.03) !important;
|
| 514 |
-
border: 1px solid rgba(255,255,255,0.08) !important;
|
| 515 |
-
border-radius: 12px !important;
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
/* ── Scrollbar ── */
|
| 519 |
-
::-webkit-scrollbar { width: 6px; }
|
| 520 |
-
::-webkit-scrollbar-track { background: transparent; }
|
| 521 |
-
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
|
| 522 |
-
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
|
| 523 |
-
</style>
|
| 524 |
-
""", unsafe_allow_html=True)
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
# ── Authentication ───────────────────────────────────────────────────────────
|
| 528 |
-
user = require_auth()
|
| 529 |
-
user_id = user["id"]
|
| 530 |
-
|
| 531 |
-
# ── Per-User Session State ───────────────────────────────────────────────────
|
| 532 |
-
if "user_data" not in st.session_state:
|
| 533 |
-
st.session_state.user_data = {}
|
| 534 |
-
|
| 535 |
-
if user_id not in st.session_state.user_data:
|
| 536 |
-
default_id = str(uuid.uuid4())
|
| 537 |
-
st.session_state.user_data[user_id] = {
|
| 538 |
-
"notebooks": {
|
| 539 |
-
default_id: {
|
| 540 |
-
"id": default_id,
|
| 541 |
-
"title": "My First Notebook",
|
| 542 |
-
"created_at": datetime.now().isoformat(),
|
| 543 |
-
"sources": [],
|
| 544 |
-
"messages": [],
|
| 545 |
-
"artifacts": [],
|
| 546 |
-
}
|
| 547 |
-
},
|
| 548 |
-
"active_notebook_id": default_id,
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
-
udata = st.session_state.user_data[user_id]
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
# ── Helper Functions ─────────────────────────────────────────────────────────
|
| 555 |
-
def get_active_notebook():
|
| 556 |
-
nb_id = udata["active_notebook_id"]
|
| 557 |
-
if nb_id and nb_id in udata["notebooks"]:
|
| 558 |
-
return udata["notebooks"][nb_id]
|
| 559 |
-
return None
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
def create_notebook(title: str):
|
| 563 |
-
nb_id = str(uuid.uuid4())
|
| 564 |
-
udata["notebooks"][nb_id] = {
|
| 565 |
-
"id": nb_id,
|
| 566 |
-
"title": title,
|
| 567 |
-
"created_at": datetime.now().isoformat(),
|
| 568 |
-
"sources": [],
|
| 569 |
-
"messages": [],
|
| 570 |
-
"artifacts": [],
|
| 571 |
-
}
|
| 572 |
-
udata["active_notebook_id"] = nb_id
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
def delete_notebook(nb_id: str):
|
| 576 |
-
if nb_id in udata["notebooks"]:
|
| 577 |
-
del udata["notebooks"][nb_id]
|
| 578 |
-
remaining = list(udata["notebooks"].keys())
|
| 579 |
-
udata["active_notebook_id"] = remaining[0] if remaining else None
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
def rename_notebook(nb_id: str, new_title: str):
|
| 583 |
-
if nb_id in udata["notebooks"]:
|
| 584 |
-
udata["notebooks"][nb_id]["title"] = new_title
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
# ── Sidebar ──────────────────────────────────────────────────────────────────
|
| 588 |
-
with st.sidebar:
|
| 589 |
-
st.markdown(
|
| 590 |
-
f'<div style="padding: 8px 0 4px 0;">'
|
| 591 |
-
f'<img src="data:image/svg+xml;base64,{LOGO_B64}" style="width:100%; max-width:240px;" />'
|
| 592 |
-
f'</div>',
|
| 593 |
-
unsafe_allow_html=True,
|
| 594 |
)
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
)
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
)
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
)
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
)
|
| 672 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
|
| 674 |
-
# ──
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
</div>
|
| 686 |
-
""",
|
| 687 |
-
unsafe_allow_html=True,
|
| 688 |
)
|
| 689 |
-
st.stop()
|
| 690 |
-
|
| 691 |
-
# ── Notebook header ──
|
| 692 |
-
source_count = len(notebook["sources"])
|
| 693 |
-
msg_count = len(notebook["messages"])
|
| 694 |
-
artifact_count = len(notebook["artifacts"])
|
| 695 |
-
st.markdown(
|
| 696 |
-
f"""
|
| 697 |
-
<div class="notebook-header">
|
| 698 |
-
<h2>{notebook["title"]}</h2>
|
| 699 |
-
<div class="meta">{source_count} sources • {msg_count} messages • {artifact_count} artifacts</div>
|
| 700 |
-
</div>
|
| 701 |
-
""",
|
| 702 |
-
unsafe_allow_html=True,
|
| 703 |
-
)
|
| 704 |
|
| 705 |
-
# ──
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
|
| 714 |
-
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
|
| 717 |
-
with tab_sources:
|
| 718 |
-
render_sources(notebook)
|
| 719 |
|
| 720 |
-
|
| 721 |
-
|
|
|
|
|
|
| 1 |
+
"""NotebookLM — AI-Powered Study Companion (Gradio)."""
|
| 2 |
+
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
from state import (
|
| 6 |
+
UserData,
|
| 7 |
+
create_default_user_data,
|
| 8 |
+
create_notebook,
|
| 9 |
+
delete_notebook,
|
| 10 |
+
rename_notebook,
|
| 11 |
+
get_active_notebook,
|
| 12 |
+
get_notebook_choices,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
)
|
| 14 |
+
from theme import dark_theme, CUSTOM_CSS, SIDEBAR_LOGO_HTML, WELCOME_HTML, NO_NOTEBOOKS_HTML
|
| 15 |
+
from pages.chat import (
|
| 16 |
+
format_chatbot_messages,
|
| 17 |
+
render_no_sources_warning,
|
| 18 |
+
handle_chat_submit,
|
| 19 |
+
handle_clear_chat,
|
| 20 |
+
)
|
| 21 |
+
from pages.sources import (
|
| 22 |
+
render_source_header,
|
| 23 |
+
render_source_list,
|
| 24 |
+
get_source_choices,
|
| 25 |
+
handle_file_upload,
|
| 26 |
+
handle_url_add,
|
| 27 |
+
handle_source_delete,
|
| 28 |
+
)
|
| 29 |
+
from pages.artifacts import (
|
| 30 |
+
render_no_sources_gate,
|
| 31 |
+
has_sources,
|
| 32 |
+
render_conv_summary_section,
|
| 33 |
+
handle_gen_conv_summary,
|
| 34 |
+
render_doc_summary_section,
|
| 35 |
+
handle_gen_doc_summary,
|
| 36 |
+
render_podcast_section,
|
| 37 |
+
handle_gen_podcast,
|
| 38 |
+
render_quiz_section,
|
| 39 |
+
handle_gen_quiz,
|
| 40 |
+
has_any_summary,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
|
| 44 |
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
| 45 |
+
|
| 46 |
+
def render_notebook_header(state: UserData) -> str:
|
| 47 |
+
nb = get_active_notebook(state)
|
| 48 |
+
if not nb:
|
| 49 |
+
return NO_NOTEBOOKS_HTML
|
| 50 |
+
src = len(nb.sources)
|
| 51 |
+
msg = len(nb.messages)
|
| 52 |
+
art = len(nb.artifacts)
|
| 53 |
+
return (
|
| 54 |
+
f'<div class="notebook-header">'
|
| 55 |
+
f'<h2>{nb.title}</h2>'
|
| 56 |
+
f'<div class="meta">{src} sources • {msg} messages • {art} artifacts</div>'
|
| 57 |
+
f'</div>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def render_user_info(state: UserData) -> str:
|
| 62 |
+
if not state:
|
| 63 |
+
return ""
|
| 64 |
+
return (
|
| 65 |
+
f'<p style="font-size:0.82rem; color:#707088; margin:4px 0 0 0;">'
|
| 66 |
+
f'Signed in as <strong style="color:#a0a0f0;">{state.user_name}</strong></p>'
|
| 67 |
)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def refresh_all(state: UserData):
|
| 71 |
+
"""Refresh all display components after state change. Returns a tuple of all outputs."""
|
| 72 |
+
nb = get_active_notebook(state)
|
| 73 |
+
choices = get_notebook_choices(state) if state else []
|
| 74 |
+
active_id = state.active_notebook_id if state else None
|
| 75 |
+
|
| 76 |
+
has_nb = nb is not None
|
| 77 |
+
has_src = has_nb and len(nb.sources) > 0
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
state,
|
| 81 |
+
# Sidebar
|
| 82 |
+
gr.update(choices=choices, value=active_id),
|
| 83 |
+
render_user_info(state),
|
| 84 |
+
# Header
|
| 85 |
+
render_notebook_header(state),
|
| 86 |
+
# Chat tab
|
| 87 |
+
format_chatbot_messages(state) if has_nb else [],
|
| 88 |
+
render_no_sources_warning(state),
|
| 89 |
+
# Sources tab
|
| 90 |
+
render_source_header(state),
|
| 91 |
+
render_source_list(state),
|
| 92 |
+
gr.update(choices=get_source_choices(state)),
|
| 93 |
+
# Artifacts tab
|
| 94 |
+
render_no_sources_gate(state),
|
| 95 |
+
gr.update(visible=has_src), # artifacts_content visible
|
| 96 |
+
gr.update(visible=not has_src), # no_sources_gate visible
|
| 97 |
+
# Artifact sub-sections
|
| 98 |
+
render_conv_summary_section(state),
|
| 99 |
+
render_doc_summary_section(state),
|
| 100 |
+
render_podcast_section(state),
|
| 101 |
+
render_quiz_section(state),
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ── Build the App ────────────────────────────────────────────────────────────
|
| 106 |
+
|
| 107 |
+
with gr.Blocks(css=CUSTOM_CSS, theme=dark_theme, title="NotebookLM") as demo:
|
| 108 |
+
|
| 109 |
+
user_state = gr.State(value=None)
|
| 110 |
+
|
| 111 |
+
# ══ Auth Gate ══════════���═════════════════════════════════════════════════
|
| 112 |
+
with gr.Column(visible=True, elem_id="auth-gate") as auth_gate:
|
| 113 |
+
gr.HTML(WELCOME_HTML)
|
| 114 |
+
gr.LoginButton(elem_id="login-btn")
|
| 115 |
+
|
| 116 |
+
# ══ Main App (hidden until login) ════════════════════════════════════════
|
| 117 |
+
with gr.Row(visible=False) as main_app:
|
| 118 |
+
|
| 119 |
+
# ── Sidebar ──────────────────────────────────────────────────────────
|
| 120 |
+
with gr.Column(scale=1, min_width=280, elem_id="sidebar"):
|
| 121 |
+
gr.HTML(SIDEBAR_LOGO_HTML)
|
| 122 |
+
user_info_html = gr.HTML("")
|
| 123 |
+
|
| 124 |
+
gr.Markdown("---")
|
| 125 |
+
|
| 126 |
+
# Create notebook
|
| 127 |
+
new_nb_name = gr.Textbox(
|
| 128 |
+
placeholder="e.g. Biology 101",
|
| 129 |
+
show_label=False,
|
| 130 |
+
container=False,
|
| 131 |
+
)
|
| 132 |
+
create_nb_btn = gr.Button("+ New Notebook", variant="primary", size="sm")
|
| 133 |
+
|
| 134 |
+
gr.HTML('<div style="height:8px;"></div>')
|
| 135 |
+
|
| 136 |
+
# Notebook selector
|
| 137 |
+
notebook_selector = gr.Radio(
|
| 138 |
+
choices=[],
|
| 139 |
+
label="Notebooks",
|
| 140 |
+
elem_id="notebook-selector",
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
gr.Markdown("---")
|
| 144 |
+
|
| 145 |
+
# Rename
|
| 146 |
+
rename_input = gr.Textbox(
|
| 147 |
+
placeholder="New name...",
|
| 148 |
+
show_label=False,
|
| 149 |
+
container=False,
|
| 150 |
+
)
|
| 151 |
+
rename_btn = gr.Button("Rename", size="sm")
|
| 152 |
+
|
| 153 |
+
# Delete
|
| 154 |
+
delete_btn = gr.Button("Delete Notebook", variant="stop", size="sm")
|
| 155 |
+
|
| 156 |
+
gr.HTML(
|
| 157 |
+
'<p style="font-size:0.75rem; color:#50506a; text-align:center; margin-top:16px;">'
|
| 158 |
+
'Built with Gradio on HF Spaces</p>'
|
| 159 |
)
|
| 160 |
+
|
| 161 |
+
# ── Main Content ─────────────────────────────────────────────────────
|
| 162 |
+
with gr.Column(scale=4, elem_id="main-content"):
|
| 163 |
+
|
| 164 |
+
notebook_header = gr.HTML(NO_NOTEBOOKS_HTML)
|
| 165 |
+
|
| 166 |
+
with gr.Tabs(elem_id="main-tabs") as main_tabs:
|
| 167 |
+
|
| 168 |
+
# ── Chat Tab ─────────────────────────────────────────────────
|
| 169 |
+
with gr.TabItem("Chat", id=0):
|
| 170 |
+
chat_warning = gr.HTML("")
|
| 171 |
+
chatbot = gr.Chatbot(
|
| 172 |
+
value=[],
|
| 173 |
+
type="messages",
|
| 174 |
+
height=480,
|
| 175 |
+
elem_id="chatbot",
|
| 176 |
+
show_label=False,
|
| 177 |
+
)
|
| 178 |
+
with gr.Row():
|
| 179 |
+
chat_input = gr.Textbox(
|
| 180 |
+
placeholder="Ask a question about your sources...",
|
| 181 |
+
show_label=False,
|
| 182 |
+
container=False,
|
| 183 |
+
scale=5,
|
| 184 |
+
)
|
| 185 |
+
clear_chat_btn = gr.Button("Clear", scale=1)
|
| 186 |
+
|
| 187 |
+
# ── Sources Tab ──────────────────────────────────────────────
|
| 188 |
+
with gr.TabItem("Sources", id=1):
|
| 189 |
+
source_header = gr.HTML("")
|
| 190 |
+
|
| 191 |
+
with gr.Row():
|
| 192 |
+
with gr.Column():
|
| 193 |
+
gr.HTML(
|
| 194 |
+
'<p style="font-weight:600; font-size:0.9rem; color:#b0b0c8; margin-bottom:8px;">'
|
| 195 |
+
'Upload Files</p>'
|
| 196 |
+
)
|
| 197 |
+
file_uploader = gr.File(
|
| 198 |
+
file_count="multiple",
|
| 199 |
+
file_types=[".pdf", ".pptx", ".txt"],
|
| 200 |
+
label="Drop files here",
|
| 201 |
+
show_label=False,
|
| 202 |
+
)
|
| 203 |
+
with gr.Column():
|
| 204 |
+
gr.HTML(
|
| 205 |
+
'<p style="font-weight:600; font-size:0.9rem; color:#b0b0c8; margin-bottom:8px;">'
|
| 206 |
+
'Add Web Source</p>'
|
| 207 |
+
)
|
| 208 |
+
url_input = gr.Textbox(
|
| 209 |
+
placeholder="https://example.com or YouTube link",
|
| 210 |
+
show_label=False,
|
| 211 |
+
container=False,
|
| 212 |
+
)
|
| 213 |
+
add_url_btn = gr.Button("Add URL", variant="primary")
|
| 214 |
+
|
| 215 |
+
gr.Markdown("---")
|
| 216 |
+
source_list_html = gr.HTML("")
|
| 217 |
+
|
| 218 |
+
with gr.Row():
|
| 219 |
+
source_selector = gr.Dropdown(
|
| 220 |
+
choices=[],
|
| 221 |
+
label="Select source to delete",
|
| 222 |
+
scale=3,
|
| 223 |
+
)
|
| 224 |
+
delete_source_btn = gr.Button("Delete Source", variant="stop", scale=1)
|
| 225 |
+
|
| 226 |
+
# ── Artifacts Tab ────────────────────────────────────────────
|
| 227 |
+
with gr.TabItem("Artifacts", id=2):
|
| 228 |
+
|
| 229 |
+
# No-sources gate
|
| 230 |
+
no_sources_msg = gr.HTML("", visible=True)
|
| 231 |
+
|
| 232 |
+
with gr.Column(visible=False) as artifacts_content:
|
| 233 |
+
|
| 234 |
+
with gr.Tabs(elem_id="artifact-tabs"):
|
| 235 |
+
|
| 236 |
+
# Summary sub-tab
|
| 237 |
+
with gr.TabItem("Summary"):
|
| 238 |
+
# Conversation Summary
|
| 239 |
+
gr.HTML(
|
| 240 |
+
'<div class="artifact-section-header">'
|
| 241 |
+
'<div class="artifact-section-icon" style="background:rgba(102,126,234,0.12);">💬</div>'
|
| 242 |
+
'<div><span style="font-weight:600; font-size:1rem; color:#e0e0f0;">Conversation Summary</span>'
|
| 243 |
+
'<p style="font-size:0.82rem; color:#808098; margin:2px 0 0 0;">'
|
| 244 |
+
'Summarize your chat history.</p></div></div>'
|
| 245 |
+
)
|
| 246 |
+
with gr.Row():
|
| 247 |
+
conv_style_radio = gr.Radio(
|
| 248 |
+
choices=["brief", "detailed"],
|
| 249 |
+
value="detailed",
|
| 250 |
+
label="Style",
|
| 251 |
+
scale=2,
|
| 252 |
+
)
|
| 253 |
+
gen_conv_sum_btn = gr.Button(
|
| 254 |
+
"Generate Conversation Summary",
|
| 255 |
+
variant="primary",
|
| 256 |
+
scale=2,
|
| 257 |
+
)
|
| 258 |
+
conv_summary_html = gr.Markdown("")
|
| 259 |
+
|
| 260 |
+
gr.HTML('<div style="margin:30px 0; border-top:1px solid rgba(255,255,255,0.06);"></div>')
|
| 261 |
+
|
| 262 |
+
# Document Summary
|
| 263 |
+
gr.HTML(
|
| 264 |
+
'<div class="artifact-section-header">'
|
| 265 |
+
'<div class="artifact-section-icon" style="background:rgba(34,197,94,0.12);">📄</div>'
|
| 266 |
+
'<div><span style="font-weight:600; font-size:1rem; color:#e0e0f0;">Document Summary</span>'
|
| 267 |
+
'<p style="font-size:0.82rem; color:#808098; margin:2px 0 0 0;">'
|
| 268 |
+
'Summarize content from your uploaded sources.</p></div></div>'
|
| 269 |
+
)
|
| 270 |
+
with gr.Row():
|
| 271 |
+
doc_style_radio = gr.Radio(
|
| 272 |
+
choices=["brief", "detailed"],
|
| 273 |
+
value="detailed",
|
| 274 |
+
label="Style",
|
| 275 |
+
scale=2,
|
| 276 |
+
)
|
| 277 |
+
gen_doc_sum_btn = gr.Button(
|
| 278 |
+
"Generate Document Summary",
|
| 279 |
+
variant="primary",
|
| 280 |
+
scale=2,
|
| 281 |
+
)
|
| 282 |
+
doc_summary_html = gr.Markdown("")
|
| 283 |
+
|
| 284 |
+
# Podcast sub-tab
|
| 285 |
+
with gr.TabItem("Podcast"):
|
| 286 |
+
gr.HTML(
|
| 287 |
+
'<div style="margin-bottom:20px;">'
|
| 288 |
+
'<span style="font-weight:600; font-size:1rem; color:#e0e0f0;">Generate Podcast</span>'
|
| 289 |
+
'<p style="font-size:0.85rem; color:#808098; margin-top:4px;">'
|
| 290 |
+
'Create a conversational podcast episode from your summary.</p></div>'
|
| 291 |
+
)
|
| 292 |
+
gen_podcast_btn = gr.Button("Generate Podcast", variant="primary")
|
| 293 |
+
podcast_html = gr.Markdown("")
|
| 294 |
+
|
| 295 |
+
# Quiz sub-tab
|
| 296 |
+
with gr.TabItem("Quiz"):
|
| 297 |
+
gr.HTML(
|
| 298 |
+
'<div style="margin-bottom:20px;">'
|
| 299 |
+
'<span style="font-weight:600; font-size:1rem; color:#e0e0f0;">Generate Quiz</span>'
|
| 300 |
+
'<p style="font-size:0.85rem; color:#808098; margin-top:4px;">'
|
| 301 |
+
'Create multiple-choice questions from your sources.</p></div>'
|
| 302 |
+
)
|
| 303 |
+
with gr.Row():
|
| 304 |
+
quiz_num_radio = gr.Radio(
|
| 305 |
+
choices=[5, 10],
|
| 306 |
+
value=5,
|
| 307 |
+
label="Number of questions",
|
| 308 |
+
scale=2,
|
| 309 |
+
)
|
| 310 |
+
gen_quiz_btn = gr.Button(
|
| 311 |
+
"Generate Quiz",
|
| 312 |
+
variant="primary",
|
| 313 |
+
scale=2,
|
| 314 |
+
)
|
| 315 |
+
quiz_html = gr.Markdown("")
|
| 316 |
+
|
| 317 |
+
# ── All refresh outputs (must match refresh_all return order) ─────────
|
| 318 |
+
refresh_outputs = [
|
| 319 |
+
user_state,
|
| 320 |
+
notebook_selector,
|
| 321 |
+
user_info_html,
|
| 322 |
+
notebook_header,
|
| 323 |
+
chatbot,
|
| 324 |
+
chat_warning,
|
| 325 |
+
source_header,
|
| 326 |
+
source_list_html,
|
| 327 |
+
source_selector,
|
| 328 |
+
no_sources_msg,
|
| 329 |
+
artifacts_content,
|
| 330 |
+
no_sources_msg,
|
| 331 |
+
conv_summary_html,
|
| 332 |
+
doc_summary_html,
|
| 333 |
+
podcast_html,
|
| 334 |
+
quiz_html,
|
| 335 |
+
]
|
| 336 |
+
|
| 337 |
+
# ══ Event Handlers ═══════════════════════════════════════════════════════
|
| 338 |
+
|
| 339 |
+
# ── Auth: on page load ───────────────────────────────────────────────────
|
| 340 |
+
def on_app_load(profile: gr.OAuthProfile | None):
|
| 341 |
+
if profile is None:
|
| 342 |
+
return None, gr.update(visible=True), gr.update(visible=False)
|
| 343 |
+
state = create_default_user_data(profile.username, profile.name)
|
| 344 |
+
return state, gr.update(visible=False), gr.update(visible=True)
|
| 345 |
+
|
| 346 |
+
demo.load(
|
| 347 |
+
fn=on_app_load,
|
| 348 |
+
inputs=None,
|
| 349 |
+
outputs=[user_state, auth_gate, main_app],
|
| 350 |
+
).then(
|
| 351 |
+
fn=refresh_all,
|
| 352 |
+
inputs=[user_state],
|
| 353 |
+
outputs=refresh_outputs,
|
| 354 |
)
|
| 355 |
|
| 356 |
+
# ── Sidebar: Create notebook ─────────────────────────────────────────────
|
| 357 |
+
def handle_create_notebook(name, state):
|
| 358 |
+
if not name or not name.strip() or not state:
|
| 359 |
+
return (state,) + refresh_all(state)[1:] + ("",)
|
| 360 |
+
state = create_notebook(state, name.strip())
|
| 361 |
+
return (state,) + refresh_all(state)[1:] + ("",)
|
| 362 |
+
|
| 363 |
+
create_nb_btn.click(
|
| 364 |
+
fn=handle_create_notebook,
|
| 365 |
+
inputs=[new_nb_name, user_state],
|
| 366 |
+
outputs=refresh_outputs + [new_nb_name],
|
| 367 |
+
)
|
| 368 |
|
| 369 |
+
# ── Sidebar: Select notebook ─────────────────────────────────────────────
|
| 370 |
+
def handle_select_notebook(nb_id, state):
|
| 371 |
+
if not state or not nb_id:
|
| 372 |
+
return refresh_all(state)
|
| 373 |
+
state.active_notebook_id = nb_id
|
| 374 |
+
return refresh_all(state)
|
| 375 |
+
|
| 376 |
+
notebook_selector.change(
|
| 377 |
+
fn=handle_select_notebook,
|
| 378 |
+
inputs=[notebook_selector, user_state],
|
| 379 |
+
outputs=refresh_outputs,
|
|
|
|
|
|
|
|
|
|
| 380 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
+
# ── Sidebar: Delete notebook ───────────────────────────────────���─────────
|
| 383 |
+
def handle_delete_notebook(state):
|
| 384 |
+
if not state or not state.active_notebook_id:
|
| 385 |
+
return refresh_all(state)
|
| 386 |
+
state = delete_notebook(state, state.active_notebook_id)
|
| 387 |
+
return refresh_all(state)
|
| 388 |
+
|
| 389 |
+
delete_btn.click(
|
| 390 |
+
fn=handle_delete_notebook,
|
| 391 |
+
inputs=[user_state],
|
| 392 |
+
outputs=refresh_outputs,
|
| 393 |
+
)
|
| 394 |
|
| 395 |
+
# ── Sidebar: Rename notebook ─────────────────────────────────────────────
|
| 396 |
+
def handle_rename_notebook(new_name, state):
|
| 397 |
+
if not state or not state.active_notebook_id or not new_name or not new_name.strip():
|
| 398 |
+
return refresh_all(state)
|
| 399 |
+
state = rename_notebook(state, state.active_notebook_id, new_name.strip())
|
| 400 |
+
return refresh_all(state)
|
| 401 |
+
|
| 402 |
+
rename_btn.click(
|
| 403 |
+
fn=handle_rename_notebook,
|
| 404 |
+
inputs=[rename_input, user_state],
|
| 405 |
+
outputs=refresh_outputs,
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# ── Chat: Submit message ─────────────────────────────────────────────────
|
| 409 |
+
chat_input.submit(
|
| 410 |
+
fn=handle_chat_submit,
|
| 411 |
+
inputs=[chat_input, user_state],
|
| 412 |
+
outputs=[user_state, chatbot, chat_input, chat_warning],
|
| 413 |
+
)
|
| 414 |
|
| 415 |
+
# ── Chat: Clear ──────────────────────────────────────────────────────────
|
| 416 |
+
clear_chat_btn.click(
|
| 417 |
+
fn=handle_clear_chat,
|
| 418 |
+
inputs=[user_state],
|
| 419 |
+
outputs=[user_state, chatbot, chat_warning],
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# ── Sources: File upload ─────────────────────────────────────────────────
|
| 423 |
+
file_uploader.upload(
|
| 424 |
+
fn=handle_file_upload,
|
| 425 |
+
inputs=[file_uploader, user_state],
|
| 426 |
+
outputs=[user_state, source_list_html, source_header, source_selector],
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
# ── Sources: Add URL ─────────────────────────────────────────────────────
|
| 430 |
+
add_url_btn.click(
|
| 431 |
+
fn=handle_url_add,
|
| 432 |
+
inputs=[url_input, user_state],
|
| 433 |
+
outputs=[user_state, source_list_html, source_header, url_input, source_selector],
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
# ── Sources: Delete source ───────────────────────────────────────────────
|
| 437 |
+
delete_source_btn.click(
|
| 438 |
+
fn=handle_source_delete,
|
| 439 |
+
inputs=[source_selector, user_state],
|
| 440 |
+
outputs=[user_state, source_list_html, source_header, source_selector],
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
# ── Artifacts: Conversation summary ──────────────────────────────────────
|
| 444 |
+
gen_conv_sum_btn.click(
|
| 445 |
+
fn=handle_gen_conv_summary,
|
| 446 |
+
inputs=[conv_style_radio, user_state],
|
| 447 |
+
outputs=[user_state, conv_summary_html],
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
# ── Artifacts: Document summary ──────────────────────────────────────────
|
| 451 |
+
gen_doc_sum_btn.click(
|
| 452 |
+
fn=handle_gen_doc_summary,
|
| 453 |
+
inputs=[doc_style_radio, user_state],
|
| 454 |
+
outputs=[user_state, doc_summary_html],
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
# ── Artifacts: Podcast ───────────────────────────────────────────────────
|
| 458 |
+
gen_podcast_btn.click(
|
| 459 |
+
fn=handle_gen_podcast,
|
| 460 |
+
inputs=[user_state],
|
| 461 |
+
outputs=[user_state, podcast_html],
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
# ── Artifacts: Quiz ──────────────────────────────────────────────────────
|
| 465 |
+
gen_quiz_btn.click(
|
| 466 |
+
fn=handle_gen_quiz,
|
| 467 |
+
inputs=[quiz_num_radio, user_state],
|
| 468 |
+
outputs=[user_state, quiz_html],
|
| 469 |
+
)
|
| 470 |
|
|
|
|
|
|
|
| 471 |
|
| 472 |
+
# ── Launch ───────────────────────────────────────────────────────────────────
|
| 473 |
+
if __name__ == "__main__":
|
| 474 |
+
demo.launch()
|
auth.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
def get_current_user() -> dict | None:
|
| 6 |
-
"""
|
| 7 |
-
Get the currently logged-in user via Streamlit's native auth (st.user).
|
| 8 |
-
|
| 9 |
-
On HF Spaces: secrets.toml is auto-generated from HF OAuth env vars
|
| 10 |
-
by entrypoint.sh, so st.login()/st.user just works.
|
| 11 |
-
Locally: uses a dev user so the app is testable without OAuth.
|
| 12 |
-
|
| 13 |
-
Returns dict with 'id' and 'name', or None if not authenticated.
|
| 14 |
-
"""
|
| 15 |
-
is_hf_space = os.environ.get("SPACE_ID") is not None
|
| 16 |
-
|
| 17 |
-
if is_hf_space:
|
| 18 |
-
if st.user.is_logged_in:
|
| 19 |
-
return {
|
| 20 |
-
"id": st.user.get("sub", st.user.get("name", "unknown")),
|
| 21 |
-
"name": st.user.get("name", st.user.get("preferred_username", "User")),
|
| 22 |
-
}
|
| 23 |
-
return None
|
| 24 |
-
else:
|
| 25 |
-
return {
|
| 26 |
-
"id": "dev_user",
|
| 27 |
-
"name": "Dev User (local)",
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def _do_login():
|
| 32 |
-
st.login("huggingface")
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def require_auth() -> dict:
|
| 36 |
-
"""
|
| 37 |
-
Gate the app behind authentication.
|
| 38 |
-
Returns user dict if authenticated, otherwise shows login prompt and stops.
|
| 39 |
-
"""
|
| 40 |
-
user = get_current_user()
|
| 41 |
-
|
| 42 |
-
if user is None:
|
| 43 |
-
st.markdown(
|
| 44 |
-
'<div style="text-align:center; padding:60px 20px;">'
|
| 45 |
-
'<h1>NotebookLM</h1>'
|
| 46 |
-
'<p style="color:#888; font-size:1.1rem;">Sign in with your Hugging Face account to continue.</p>'
|
| 47 |
-
"</div>",
|
| 48 |
-
unsafe_allow_html=True,
|
| 49 |
-
)
|
| 50 |
-
_col1, col2, _col3 = st.columns([1, 1, 1])
|
| 51 |
-
with col2:
|
| 52 |
-
st.button(
|
| 53 |
-
"Sign in with Hugging Face",
|
| 54 |
-
on_click=_do_login,
|
| 55 |
-
use_container_width=True,
|
| 56 |
-
type="primary",
|
| 57 |
-
)
|
| 58 |
-
st.stop()
|
| 59 |
-
|
| 60 |
-
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
entrypoint.sh
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
set -e
|
| 3 |
-
|
| 4 |
-
# Generate .streamlit/secrets.toml from HF OAuth environment variables
|
| 5 |
-
mkdir -p /app/.streamlit
|
| 6 |
-
|
| 7 |
-
REDIRECT="https://${SPACE_HOST}/oauth2callback"
|
| 8 |
-
|
| 9 |
-
cat > /app/.streamlit/secrets.toml <<EOF
|
| 10 |
-
[auth]
|
| 11 |
-
redirect_uri = "${REDIRECT}"
|
| 12 |
-
cookie_secret = "$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
|
| 13 |
-
|
| 14 |
-
[auth.huggingface]
|
| 15 |
-
client_id = "${OAUTH_CLIENT_ID}"
|
| 16 |
-
client_secret = "${OAUTH_CLIENT_SECRET}"
|
| 17 |
-
server_metadata_url = "${OPENID_PROVIDER_URL}/.well-known/openid-configuration"
|
| 18 |
-
EOF
|
| 19 |
-
|
| 20 |
-
echo "Generated secrets.toml with redirect_uri=${REDIRECT}"
|
| 21 |
-
|
| 22 |
-
exec streamlit run app.py \
|
| 23 |
-
--server.port=8501 \
|
| 24 |
-
--server.address=0.0.0.0 \
|
| 25 |
-
--server.enableCORS=false \
|
| 26 |
-
--server.enableXsrfProtection=false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/artifact_page.py → mock_data.py
RENAMED
|
@@ -1,10 +1,69 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
| 2 |
import uuid
|
| 3 |
from datetime import datetime
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
# ── Mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
MOCK_CONVERSATION_SUMMARY = {
|
| 10 |
"brief": """## Conversation Summary (Brief)
|
|
@@ -211,7 +270,6 @@ What is the recommended learning order for the three methods?
|
|
| 211 |
|
| 212 |
### Question 1
|
| 213 |
What is the primary advantage of the direct method?
|
| 214 |
-
|
| 215 |
- A) It works with incomplete data
|
| 216 |
- B) It provides exact results in a single pass
|
| 217 |
- C) It is the fastest method
|
|
@@ -223,7 +281,6 @@ What is the primary advantage of the direct method?
|
|
| 223 |
|
| 224 |
### Question 2
|
| 225 |
Which stage comes immediately after initialization?
|
| 226 |
-
|
| 227 |
- A) Validation
|
| 228 |
- B) Optimization
|
| 229 |
- C) Processing
|
|
@@ -235,7 +292,6 @@ Which stage comes immediately after initialization?
|
|
| 235 |
|
| 236 |
### Question 3
|
| 237 |
When should the approximation method be preferred?
|
| 238 |
-
|
| 239 |
- A) When working with critical systems
|
| 240 |
- B) When precision is not the primary concern
|
| 241 |
- C) When the dataset is very small
|
|
@@ -247,7 +303,6 @@ When should the approximation method be preferred?
|
|
| 247 |
|
| 248 |
### Question 4
|
| 249 |
What does "convergence" mean in the iterative approach?
|
| 250 |
-
|
| 251 |
- A) The point where the algorithm starts
|
| 252 |
- B) When iterations produce negligible differences
|
| 253 |
- C) The final validation step
|
|
@@ -259,7 +314,6 @@ What does "convergence" mean in the iterative approach?
|
|
| 259 |
|
| 260 |
### Question 5
|
| 261 |
What is the recommended learning order?
|
| 262 |
-
|
| 263 |
- A) Approximation → Iterative → Direct
|
| 264 |
- B) Iterative → Direct → Approximation
|
| 265 |
- C) Direct → Iterative → Approximation
|
|
@@ -271,7 +325,6 @@ What is the recommended learning order?
|
|
| 271 |
|
| 272 |
### Question 6
|
| 273 |
What does the direct method require?
|
| 274 |
-
|
| 275 |
- A) Multiple iterations
|
| 276 |
- B) Complete data
|
| 277 |
- C) A confidence threshold
|
|
@@ -283,7 +336,6 @@ What does the direct method require?
|
|
| 283 |
|
| 284 |
### Question 7
|
| 285 |
What is a "confidence threshold"?
|
| 286 |
-
|
| 287 |
- A) How sure you are about your method choice
|
| 288 |
- B) The minimum acceptable certainty for a result
|
| 289 |
- C) The maximum number of iterations
|
|
@@ -295,7 +347,6 @@ What is a "confidence threshold"?
|
|
| 295 |
|
| 296 |
### Question 8
|
| 297 |
How do lecture notes differ from the textbook?
|
| 298 |
-
|
| 299 |
- A) Lectures focus on theory, textbook on application
|
| 300 |
- B) Lectures focus on application, textbook on theory
|
| 301 |
- C) They cover different topics entirely
|
|
@@ -307,7 +358,6 @@ How do lecture notes differ from the textbook?
|
|
| 307 |
|
| 308 |
### Question 9
|
| 309 |
What is a "base case" in this context?
|
| 310 |
-
|
| 311 |
- A) The most complex problem instance
|
| 312 |
- B) The simplest problem instance used as a starting point
|
| 313 |
- C) The final validated result
|
|
@@ -319,7 +369,6 @@ What is a "base case" in this context?
|
|
| 319 |
|
| 320 |
### Question 10
|
| 321 |
The three-stage process applies to which methods?
|
| 322 |
-
|
| 323 |
- A) Only the direct method
|
| 324 |
- B) Only the iterative method
|
| 325 |
- C) Direct and iterative only
|
|
@@ -330,21 +379,7 @@ The three-stage process applies to which methods?
|
|
| 330 |
}
|
| 331 |
|
| 332 |
|
| 333 |
-
def
|
| 334 |
-
"""Get the most recent artifact of a given type."""
|
| 335 |
-
for artifact in reversed(notebook["artifacts"]):
|
| 336 |
-
if artifact["type"] == artifact_type:
|
| 337 |
-
return artifact
|
| 338 |
-
return None
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
def get_all_artifacts(notebook: dict, artifact_type: str) -> list[dict]:
|
| 342 |
-
"""Get all artifacts of a given type, newest first."""
|
| 343 |
-
return [a for a in reversed(notebook["artifacts"]) if a["type"] == artifact_type]
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
def generate_mock_artifact(artifact_type: str, **kwargs) -> dict:
|
| 347 |
-
"""Simulate artifact generation."""
|
| 348 |
time.sleep(1.5)
|
| 349 |
|
| 350 |
if artifact_type == "conversation_summary":
|
|
@@ -366,465 +401,11 @@ def generate_mock_artifact(artifact_type: str, **kwargs) -> dict:
|
|
| 366 |
content = ""
|
| 367 |
title = artifact_type
|
| 368 |
|
| 369 |
-
return
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
def _render_artifact_card(artifact: dict, index: int, notebook: dict):
|
| 380 |
-
"""Render a single artifact with actions."""
|
| 381 |
-
try:
|
| 382 |
-
dt = datetime.fromisoformat(artifact["created_at"])
|
| 383 |
-
time_str = dt.strftime("%b %d at %H:%M")
|
| 384 |
-
except (ValueError, KeyError):
|
| 385 |
-
time_str = ""
|
| 386 |
-
|
| 387 |
-
st.markdown(
|
| 388 |
-
f"""
|
| 389 |
-
<div style="
|
| 390 |
-
padding: 4px 0 8px 0;
|
| 391 |
-
font-size: 0.8rem;
|
| 392 |
-
color: #707088;
|
| 393 |
-
">Generated {time_str}</div>
|
| 394 |
-
""",
|
| 395 |
-
unsafe_allow_html=True,
|
| 396 |
)
|
| 397 |
-
|
| 398 |
-
# Content
|
| 399 |
-
with st.container(height=400, border=True):
|
| 400 |
-
st.markdown(artifact["content"])
|
| 401 |
-
|
| 402 |
-
# Audio player for podcast
|
| 403 |
-
if artifact["type"] == "podcast":
|
| 404 |
-
if artifact.get("audio_path"):
|
| 405 |
-
st.audio(artifact["audio_path"])
|
| 406 |
-
else:
|
| 407 |
-
st.markdown(
|
| 408 |
-
"""
|
| 409 |
-
<div style="
|
| 410 |
-
display: flex; align-items: center; gap: 10px;
|
| 411 |
-
padding: 12px 16px;
|
| 412 |
-
background: rgba(102,126,234,0.06);
|
| 413 |
-
border: 1px solid rgba(102,126,234,0.15);
|
| 414 |
-
border-radius: 10px;
|
| 415 |
-
margin-top: 8px;
|
| 416 |
-
">
|
| 417 |
-
<span style="font-size: 1.3rem;">🔇</span>
|
| 418 |
-
<span style="font-size: 0.85rem; color: #8888aa;">
|
| 419 |
-
Audio player will appear here when TTS is connected.
|
| 420 |
-
</span>
|
| 421 |
-
</div>
|
| 422 |
-
""",
|
| 423 |
-
unsafe_allow_html=True,
|
| 424 |
-
)
|
| 425 |
-
|
| 426 |
-
# Actions
|
| 427 |
-
st.markdown('<div style="margin-top: 8px;"></div>', unsafe_allow_html=True)
|
| 428 |
-
c1, c2, c3 = st.columns([1, 1, 3])
|
| 429 |
-
with c1:
|
| 430 |
-
st.download_button(
|
| 431 |
-
"Download .md",
|
| 432 |
-
data=artifact["content"],
|
| 433 |
-
file_name=f"{artifact['title'].lower().replace(' ', '_')}.md",
|
| 434 |
-
mime="text/markdown",
|
| 435 |
-
key=f"dl_{artifact['id']}",
|
| 436 |
-
use_container_width=True,
|
| 437 |
-
)
|
| 438 |
-
with c2:
|
| 439 |
-
if st.button("Delete", key=f"del_{artifact['id']}", use_container_width=True):
|
| 440 |
-
notebook["artifacts"].remove(artifact)
|
| 441 |
-
st.rerun()
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
def _render_history(artifacts: list[dict], label: str):
|
| 445 |
-
"""Show older artifacts in a collapsed section."""
|
| 446 |
-
if len(artifacts) > 1:
|
| 447 |
-
with st.expander(f"Previous {label} ({len(artifacts) - 1})"):
|
| 448 |
-
for a in artifacts[1:]:
|
| 449 |
-
try:
|
| 450 |
-
dt = datetime.fromisoformat(a["created_at"])
|
| 451 |
-
time_str = dt.strftime("%b %d at %H:%M")
|
| 452 |
-
except (ValueError, KeyError):
|
| 453 |
-
time_str = ""
|
| 454 |
-
st.markdown(f"**{a['title']}** — {time_str}")
|
| 455 |
-
with st.container(height=200, border=True):
|
| 456 |
-
st.markdown(a["content"])
|
| 457 |
-
c1, c2, c3 = st.columns([1, 1, 4])
|
| 458 |
-
with c1:
|
| 459 |
-
st.download_button(
|
| 460 |
-
"Download",
|
| 461 |
-
data=a["content"],
|
| 462 |
-
file_name=f"{a['title'].lower().replace(' ', '_')}.md",
|
| 463 |
-
mime="text/markdown",
|
| 464 |
-
key=f"dl_hist_{a['id']}",
|
| 465 |
-
use_container_width=True,
|
| 466 |
-
)
|
| 467 |
-
with c2:
|
| 468 |
-
# No delete in history to keep things simple
|
| 469 |
-
pass
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
# ── Main Render ──────────────────────────────────────────────────────────────
|
| 473 |
-
|
| 474 |
-
def render_artifacts(notebook: dict):
|
| 475 |
-
"""Render artifact generation with sub-tabs: Summary | Podcast | Quiz."""
|
| 476 |
-
|
| 477 |
-
if not notebook["sources"]:
|
| 478 |
-
st.markdown(
|
| 479 |
-
"""
|
| 480 |
-
<div class="empty-state">
|
| 481 |
-
<div style="font-size: 3rem; margin-bottom: 16px;">🎯</div>
|
| 482 |
-
<h3>Add sources first</h3>
|
| 483 |
-
<p>Upload documents in the <strong>Sources</strong> tab to unlock<br>
|
| 484 |
-
summary, quiz, and podcast generation.</p>
|
| 485 |
-
</div>
|
| 486 |
-
""",
|
| 487 |
-
unsafe_allow_html=True,
|
| 488 |
-
)
|
| 489 |
-
return
|
| 490 |
-
|
| 491 |
-
tab_summary, tab_podcast, tab_quiz = st.tabs(
|
| 492 |
-
[" Summary ", " Podcast ", " Quiz "]
|
| 493 |
-
)
|
| 494 |
-
|
| 495 |
-
# ── SUMMARY TAB ──────────────────────────────────────────────────────────
|
| 496 |
-
with tab_summary:
|
| 497 |
-
|
| 498 |
-
# ── Section 1: Conversation Summary ──
|
| 499 |
-
st.markdown(
|
| 500 |
-
"""
|
| 501 |
-
<div style="
|
| 502 |
-
display: flex; align-items: center; gap: 10px;
|
| 503 |
-
margin-bottom: 12px;
|
| 504 |
-
">
|
| 505 |
-
<div style="
|
| 506 |
-
width: 36px; height: 36px; border-radius: 10px;
|
| 507 |
-
background: rgba(102,126,234,0.12);
|
| 508 |
-
display: flex; align-items: center; justify-content: center;
|
| 509 |
-
font-size: 1.1rem;
|
| 510 |
-
">💬</div>
|
| 511 |
-
<div>
|
| 512 |
-
<span style="font-weight:600; font-size:1rem; color:#e0e0f0;">
|
| 513 |
-
Conversation Summary
|
| 514 |
-
</span>
|
| 515 |
-
<p style="font-size:0.82rem; color:#808098; margin:2px 0 0 0;">
|
| 516 |
-
Summarize your chat history — topics discussed, key insights, and citations used.
|
| 517 |
-
</p>
|
| 518 |
-
</div>
|
| 519 |
-
</div>
|
| 520 |
-
""",
|
| 521 |
-
unsafe_allow_html=True,
|
| 522 |
-
)
|
| 523 |
-
|
| 524 |
-
has_messages = len(notebook["messages"]) > 0
|
| 525 |
-
|
| 526 |
-
if not has_messages:
|
| 527 |
-
st.markdown(
|
| 528 |
-
"""
|
| 529 |
-
<div style="
|
| 530 |
-
text-align: center; padding: 30px 20px;
|
| 531 |
-
color: #606078;
|
| 532 |
-
border: 1px dashed rgba(255,255,255,0.08);
|
| 533 |
-
border-radius: 14px;
|
| 534 |
-
">
|
| 535 |
-
<p style="margin:0;">No conversation yet. Start chatting in the <strong>Chat</strong> tab first.</p>
|
| 536 |
-
</div>
|
| 537 |
-
""",
|
| 538 |
-
unsafe_allow_html=True,
|
| 539 |
-
)
|
| 540 |
-
else:
|
| 541 |
-
col_cs1, col_cs2, col_cs3 = st.columns([2, 2, 2])
|
| 542 |
-
with col_cs1:
|
| 543 |
-
conv_style = st.radio(
|
| 544 |
-
"Style",
|
| 545 |
-
["brief", "detailed"],
|
| 546 |
-
format_func=lambda x: "Brief" if x == "brief" else "Detailed",
|
| 547 |
-
horizontal=True,
|
| 548 |
-
key=f"conv_sum_style_{notebook['id']}",
|
| 549 |
-
)
|
| 550 |
-
with col_cs3:
|
| 551 |
-
st.markdown('<div style="margin-top: 24px;"></div>', unsafe_allow_html=True)
|
| 552 |
-
gen_conv_sum = st.button(
|
| 553 |
-
"Generate Conversation Summary",
|
| 554 |
-
type="primary",
|
| 555 |
-
use_container_width=True,
|
| 556 |
-
key=f"gen_conv_sum_{notebook['id']}",
|
| 557 |
-
)
|
| 558 |
-
|
| 559 |
-
if gen_conv_sum:
|
| 560 |
-
with st.spinner("Summarizing conversation..."):
|
| 561 |
-
artifact = generate_mock_artifact("conversation_summary", style=conv_style)
|
| 562 |
-
notebook["artifacts"].append(artifact)
|
| 563 |
-
st.rerun()
|
| 564 |
-
|
| 565 |
-
conv_summaries = get_all_artifacts(notebook, "conversation_summary")
|
| 566 |
-
if conv_summaries:
|
| 567 |
-
_render_artifact_card(conv_summaries[0], 0, notebook)
|
| 568 |
-
_render_history(conv_summaries, "conversation summaries")
|
| 569 |
-
|
| 570 |
-
# ── Divider between sections ──
|
| 571 |
-
st.markdown(
|
| 572 |
-
'<div style="margin: 30px 0; border-top: 1px solid rgba(255,255,255,0.06);"></div>',
|
| 573 |
-
unsafe_allow_html=True,
|
| 574 |
-
)
|
| 575 |
-
|
| 576 |
-
# ── Section 2: Document Summary ──
|
| 577 |
-
st.markdown(
|
| 578 |
-
"""
|
| 579 |
-
<div style="
|
| 580 |
-
display: flex; align-items: center; gap: 10px;
|
| 581 |
-
margin-bottom: 12px;
|
| 582 |
-
">
|
| 583 |
-
<div style="
|
| 584 |
-
width: 36px; height: 36px; border-radius: 10px;
|
| 585 |
-
background: rgba(34,197,94,0.12);
|
| 586 |
-
display: flex; align-items: center; justify-content: center;
|
| 587 |
-
font-size: 1.1rem;
|
| 588 |
-
">📄</div>
|
| 589 |
-
<div>
|
| 590 |
-
<span style="font-weight:600; font-size:1rem; color:#e0e0f0;">
|
| 591 |
-
Document Summary
|
| 592 |
-
</span>
|
| 593 |
-
<p style="font-size:0.82rem; color:#808098; margin:2px 0 0 0;">
|
| 594 |
-
Summarize content from your uploaded sources — key concepts, themes, and connections.
|
| 595 |
-
</p>
|
| 596 |
-
</div>
|
| 597 |
-
</div>
|
| 598 |
-
""",
|
| 599 |
-
unsafe_allow_html=True,
|
| 600 |
-
)
|
| 601 |
-
|
| 602 |
-
col_ds1, col_ds2, col_ds3 = st.columns([2, 2, 2])
|
| 603 |
-
with col_ds1:
|
| 604 |
-
doc_style = st.radio(
|
| 605 |
-
"Style",
|
| 606 |
-
["brief", "detailed"],
|
| 607 |
-
format_func=lambda x: "Brief (1 page)" if x == "brief" else "Detailed (full analysis)",
|
| 608 |
-
horizontal=True,
|
| 609 |
-
key=f"doc_sum_style_{notebook['id']}",
|
| 610 |
-
)
|
| 611 |
-
with col_ds3:
|
| 612 |
-
st.markdown('<div style="margin-top: 24px;"></div>', unsafe_allow_html=True)
|
| 613 |
-
gen_doc_sum = st.button(
|
| 614 |
-
"Generate Document Summary",
|
| 615 |
-
type="primary",
|
| 616 |
-
use_container_width=True,
|
| 617 |
-
key=f"gen_doc_sum_{notebook['id']}",
|
| 618 |
-
)
|
| 619 |
-
|
| 620 |
-
if gen_doc_sum:
|
| 621 |
-
with st.spinner("Analyzing sources and generating summary..."):
|
| 622 |
-
artifact = generate_mock_artifact("document_summary", style=doc_style)
|
| 623 |
-
notebook["artifacts"].append(artifact)
|
| 624 |
-
st.rerun()
|
| 625 |
-
|
| 626 |
-
doc_summaries = get_all_artifacts(notebook, "document_summary")
|
| 627 |
-
if doc_summaries:
|
| 628 |
-
_render_artifact_card(doc_summaries[0], 0, notebook)
|
| 629 |
-
_render_history(doc_summaries, "document summaries")
|
| 630 |
-
|
| 631 |
-
# ── PODCAST TAB ──────────────────────────────────────────────────────────
|
| 632 |
-
with tab_podcast:
|
| 633 |
-
# Podcast depends on having any summary (conversation or document)
|
| 634 |
-
latest_doc_summary = get_latest_artifact(notebook, "document_summary")
|
| 635 |
-
latest_conv_summary = get_latest_artifact(notebook, "conversation_summary")
|
| 636 |
-
latest_summary = latest_doc_summary or latest_conv_summary
|
| 637 |
-
has_summary = latest_summary is not None
|
| 638 |
-
|
| 639 |
-
st.markdown(
|
| 640 |
-
"""
|
| 641 |
-
<div style="margin-bottom: 20px;">
|
| 642 |
-
<span style="font-weight:600; font-size:1rem; color:#e0e0f0;">
|
| 643 |
-
Generate Podcast
|
| 644 |
-
</span>
|
| 645 |
-
<p style="font-size:0.85rem; color:#808098; margin-top:4px;">
|
| 646 |
-
Create a conversational podcast episode from your summary.
|
| 647 |
-
</p>
|
| 648 |
-
</div>
|
| 649 |
-
""",
|
| 650 |
-
unsafe_allow_html=True,
|
| 651 |
-
)
|
| 652 |
-
|
| 653 |
-
if not has_summary:
|
| 654 |
-
# Locked state
|
| 655 |
-
st.markdown(
|
| 656 |
-
"""
|
| 657 |
-
<div style="
|
| 658 |
-
text-align: center;
|
| 659 |
-
padding: 50px 30px;
|
| 660 |
-
background: rgba(255,255,255,0.02);
|
| 661 |
-
border: 1px solid rgba(255,255,255,0.06);
|
| 662 |
-
border-radius: 16px;
|
| 663 |
-
">
|
| 664 |
-
<div style="font-size: 2.5rem; margin-bottom: 16px;">🔒</div>
|
| 665 |
-
<h3 style="color: #a0a0b8; font-weight: 600; margin-bottom: 8px;">
|
| 666 |
-
Summary Required
|
| 667 |
-
</h3>
|
| 668 |
-
<p style="color: #707088; font-size: 0.9rem; line-height: 1.6;">
|
| 669 |
-
Generate a summary first in the <strong>Summary</strong> tab.<br>
|
| 670 |
-
The podcast is created from your summary to ensure accuracy.
|
| 671 |
-
</p>
|
| 672 |
-
<div style="
|
| 673 |
-
margin-top: 20px;
|
| 674 |
-
display: inline-flex; align-items: center; gap: 8px;
|
| 675 |
-
padding: 8px 18px;
|
| 676 |
-
background: rgba(102,126,234,0.08);
|
| 677 |
-
border: 1px solid rgba(102,126,234,0.15);
|
| 678 |
-
border-radius: 20px;
|
| 679 |
-
font-size: 0.82rem;
|
| 680 |
-
color: #8090d0;
|
| 681 |
-
">
|
| 682 |
-
📝 Summary → 🎙️ Podcast
|
| 683 |
-
</div>
|
| 684 |
-
</div>
|
| 685 |
-
""",
|
| 686 |
-
unsafe_allow_html=True,
|
| 687 |
-
)
|
| 688 |
-
else:
|
| 689 |
-
# Show which summary will be used
|
| 690 |
-
sum_title = latest_summary["title"]
|
| 691 |
-
try:
|
| 692 |
-
dt = datetime.fromisoformat(latest_summary["created_at"])
|
| 693 |
-
sum_time = dt.strftime("%b %d at %H:%M")
|
| 694 |
-
except (ValueError, KeyError):
|
| 695 |
-
sum_time = ""
|
| 696 |
-
|
| 697 |
-
st.markdown(
|
| 698 |
-
f"""
|
| 699 |
-
<div style="
|
| 700 |
-
display: flex; align-items: center; gap: 12px;
|
| 701 |
-
padding: 12px 18px;
|
| 702 |
-
background: rgba(34,197,94,0.06);
|
| 703 |
-
border: 1px solid rgba(34,197,94,0.15);
|
| 704 |
-
border-radius: 12px;
|
| 705 |
-
margin-bottom: 16px;
|
| 706 |
-
">
|
| 707 |
-
<span style="font-size: 1.2rem;">📝</span>
|
| 708 |
-
<div>
|
| 709 |
-
<span style="font-size: 0.85rem; color: #a0b8a0;">
|
| 710 |
-
Based on:
|
| 711 |
-
</span>
|
| 712 |
-
<strong style="color: #c0e0c0;">{sum_title}</strong>
|
| 713 |
-
<span style="color: #708070; font-size: 0.8rem;">
|
| 714 |
-
({sum_time})
|
| 715 |
-
</span>
|
| 716 |
-
</div>
|
| 717 |
-
</div>
|
| 718 |
-
""",
|
| 719 |
-
unsafe_allow_html=True,
|
| 720 |
-
)
|
| 721 |
-
|
| 722 |
-
col1, col2 = st.columns([3, 1])
|
| 723 |
-
with col2:
|
| 724 |
-
gen_podcast = st.button(
|
| 725 |
-
"Generate Podcast",
|
| 726 |
-
type="primary",
|
| 727 |
-
use_container_width=True,
|
| 728 |
-
key=f"gen_pod_{notebook['id']}",
|
| 729 |
-
)
|
| 730 |
-
|
| 731 |
-
if gen_podcast:
|
| 732 |
-
with st.spinner("Creating podcast script and audio..."):
|
| 733 |
-
artifact = generate_mock_artifact("podcast")
|
| 734 |
-
notebook["artifacts"].append(artifact)
|
| 735 |
-
st.rerun()
|
| 736 |
-
|
| 737 |
-
# Display latest podcast
|
| 738 |
-
podcasts = get_all_artifacts(notebook, "podcast")
|
| 739 |
-
if podcasts:
|
| 740 |
-
st.divider()
|
| 741 |
-
st.markdown(
|
| 742 |
-
f'<span style="font-weight:600; font-size:0.9rem; color:#b0b0c8;">'
|
| 743 |
-
f'Latest Podcast</span>',
|
| 744 |
-
unsafe_allow_html=True,
|
| 745 |
-
)
|
| 746 |
-
_render_artifact_card(podcasts[0], 0, notebook)
|
| 747 |
-
_render_history(podcasts, "podcasts")
|
| 748 |
-
else:
|
| 749 |
-
st.markdown(
|
| 750 |
-
"""
|
| 751 |
-
<div style="
|
| 752 |
-
text-align: center; padding: 40px 20px;
|
| 753 |
-
color: #606078; margin-top: 16px;
|
| 754 |
-
border: 1px dashed rgba(255,255,255,0.08);
|
| 755 |
-
border-radius: 14px;
|
| 756 |
-
">
|
| 757 |
-
<div style="font-size: 2rem; margin-bottom: 10px;">🎙️</div>
|
| 758 |
-
<p>No podcast generated yet.<br>
|
| 759 |
-
Click <strong>Generate Podcast</strong> to create one from your summary.</p>
|
| 760 |
-
</div>
|
| 761 |
-
""",
|
| 762 |
-
unsafe_allow_html=True,
|
| 763 |
-
)
|
| 764 |
-
|
| 765 |
-
# ── QUIZ TAB ─────────────────────────────────────────────────────────────
|
| 766 |
-
with tab_quiz:
|
| 767 |
-
st.markdown(
|
| 768 |
-
"""
|
| 769 |
-
<div style="margin-bottom: 20px;">
|
| 770 |
-
<span style="font-weight:600; font-size:1rem; color:#e0e0f0;">
|
| 771 |
-
Generate Quiz
|
| 772 |
-
</span>
|
| 773 |
-
<p style="font-size:0.85rem; color:#808098; margin-top:4px;">
|
| 774 |
-
Create multiple-choice questions from your sources to test your understanding.
|
| 775 |
-
</p>
|
| 776 |
-
</div>
|
| 777 |
-
""",
|
| 778 |
-
unsafe_allow_html=True,
|
| 779 |
-
)
|
| 780 |
-
|
| 781 |
-
col_q1, col_q2, col_q3 = st.columns([2, 2, 2])
|
| 782 |
-
with col_q1:
|
| 783 |
-
num_questions = st.select_slider(
|
| 784 |
-
"Number of questions",
|
| 785 |
-
options=[5, 10],
|
| 786 |
-
value=5,
|
| 787 |
-
key=f"quiz_num_{notebook['id']}",
|
| 788 |
-
)
|
| 789 |
-
with col_q3:
|
| 790 |
-
st.markdown('<div style="margin-top: 24px;"></div>', unsafe_allow_html=True)
|
| 791 |
-
gen_quiz = st.button(
|
| 792 |
-
"Generate Quiz",
|
| 793 |
-
type="primary",
|
| 794 |
-
use_container_width=True,
|
| 795 |
-
key=f"gen_quiz_{notebook['id']}",
|
| 796 |
-
)
|
| 797 |
-
|
| 798 |
-
if gen_quiz:
|
| 799 |
-
with st.spinner(f"Generating {num_questions} questions..."):
|
| 800 |
-
artifact = generate_mock_artifact("quiz", num_questions=num_questions)
|
| 801 |
-
notebook["artifacts"].append(artifact)
|
| 802 |
-
st.rerun()
|
| 803 |
-
|
| 804 |
-
# Display latest quiz
|
| 805 |
-
quizzes = get_all_artifacts(notebook, "quiz")
|
| 806 |
-
if quizzes:
|
| 807 |
-
st.divider()
|
| 808 |
-
st.markdown(
|
| 809 |
-
f'<span style="font-weight:600; font-size:0.9rem; color:#b0b0c8;">'
|
| 810 |
-
f'Latest Quiz</span>',
|
| 811 |
-
unsafe_allow_html=True,
|
| 812 |
-
)
|
| 813 |
-
_render_artifact_card(quizzes[0], 0, notebook)
|
| 814 |
-
_render_history(quizzes, "quizzes")
|
| 815 |
-
else:
|
| 816 |
-
st.markdown(
|
| 817 |
-
"""
|
| 818 |
-
<div style="
|
| 819 |
-
text-align: center; padding: 40px 20px;
|
| 820 |
-
color: #606078; margin-top: 16px;
|
| 821 |
-
border: 1px dashed rgba(255,255,255,0.08);
|
| 822 |
-
border-radius: 14px;
|
| 823 |
-
">
|
| 824 |
-
<div style="font-size: 2rem; margin-bottom: 10px;">❓</div>
|
| 825 |
-
<p>No quiz generated yet.<br>
|
| 826 |
-
Choose the number of questions and click <strong>Generate Quiz</strong>.</p>
|
| 827 |
-
</div>
|
| 828 |
-
""",
|
| 829 |
-
unsafe_allow_html=True,
|
| 830 |
-
)
|
|
|
|
| 1 |
+
"""All mock responses and content for the NotebookLM prototype."""
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
import time
|
| 5 |
import uuid
|
| 6 |
from datetime import datetime
|
| 7 |
+
from state import Artifact
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# ── Chat Mock Responses ──────────────────────────────────────────────────────
|
| 11 |
+
|
| 12 |
+
MOCK_RESPONSES = [
|
| 13 |
+
{
|
| 14 |
+
"content": (
|
| 15 |
+
"Based on the uploaded sources, the key concept revolves around "
|
| 16 |
+
"the relationship between the variables discussed in Chapter 3. "
|
| 17 |
+
"The author emphasizes that understanding this foundation is critical "
|
| 18 |
+
"before moving to advanced topics."
|
| 19 |
+
),
|
| 20 |
+
"citations": [
|
| 21 |
+
{"source": "lecture_notes.pdf", "page": 3, "text": "the relationship between variables..."},
|
| 22 |
+
{"source": "textbook_ch3.pdf", "page": 42, "text": "understanding this foundation..."},
|
| 23 |
+
],
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"content": (
|
| 27 |
+
"The sources indicate three main approaches to this problem:\n\n"
|
| 28 |
+
"1. **Direct method** — Apply the formula from Section 2.1\n"
|
| 29 |
+
"2. **Iterative approach** — Build up from base cases\n"
|
| 30 |
+
"3. **Approximation** — Use the simplified model when precision isn't critical\n\n"
|
| 31 |
+
"The textbook recommends starting with the direct method for beginners."
|
| 32 |
+
),
|
| 33 |
+
"citations": [
|
| 34 |
+
{"source": "textbook_ch2.pdf", "page": 15, "text": "direct method... apply the formula"},
|
| 35 |
+
],
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"content": (
|
| 39 |
+
"I couldn't find specific information about that topic in your "
|
| 40 |
+
"uploaded sources. Try uploading additional materials that cover this "
|
| 41 |
+
"subject, or rephrase your question to relate more closely to the "
|
| 42 |
+
"content in your current sources."
|
| 43 |
+
),
|
| 44 |
+
"citations": [],
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"content": (
|
| 48 |
+
"Great question! According to the lecture slides, this concept "
|
| 49 |
+
"was introduced in Week 5. The key takeaway is that the process involves "
|
| 50 |
+
"three stages: **initialization**, **processing**, and **validation**. "
|
| 51 |
+
"Each stage has specific requirements that must be met before proceeding."
|
| 52 |
+
),
|
| 53 |
+
"citations": [
|
| 54 |
+
{"source": "week5_slides.pptx", "page": 8, "text": "three stages: initialization..."},
|
| 55 |
+
{"source": "week5_slides.pptx", "page": 12, "text": "specific requirements..."},
|
| 56 |
+
],
|
| 57 |
+
},
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def get_mock_response(query: str) -> dict:
|
| 62 |
+
time.sleep(1.2)
|
| 63 |
+
return random.choice(MOCK_RESPONSES)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ── Artifact Mock Content ────────────────────────────────────────────────────
|
| 67 |
|
| 68 |
MOCK_CONVERSATION_SUMMARY = {
|
| 69 |
"brief": """## Conversation Summary (Brief)
|
|
|
|
| 270 |
|
| 271 |
### Question 1
|
| 272 |
What is the primary advantage of the direct method?
|
|
|
|
| 273 |
- A) It works with incomplete data
|
| 274 |
- B) It provides exact results in a single pass
|
| 275 |
- C) It is the fastest method
|
|
|
|
| 281 |
|
| 282 |
### Question 2
|
| 283 |
Which stage comes immediately after initialization?
|
|
|
|
| 284 |
- A) Validation
|
| 285 |
- B) Optimization
|
| 286 |
- C) Processing
|
|
|
|
| 292 |
|
| 293 |
### Question 3
|
| 294 |
When should the approximation method be preferred?
|
|
|
|
| 295 |
- A) When working with critical systems
|
| 296 |
- B) When precision is not the primary concern
|
| 297 |
- C) When the dataset is very small
|
|
|
|
| 303 |
|
| 304 |
### Question 4
|
| 305 |
What does "convergence" mean in the iterative approach?
|
|
|
|
| 306 |
- A) The point where the algorithm starts
|
| 307 |
- B) When iterations produce negligible differences
|
| 308 |
- C) The final validation step
|
|
|
|
| 314 |
|
| 315 |
### Question 5
|
| 316 |
What is the recommended learning order?
|
|
|
|
| 317 |
- A) Approximation → Iterative → Direct
|
| 318 |
- B) Iterative → Direct → Approximation
|
| 319 |
- C) Direct → Iterative → Approximation
|
|
|
|
| 325 |
|
| 326 |
### Question 6
|
| 327 |
What does the direct method require?
|
|
|
|
| 328 |
- A) Multiple iterations
|
| 329 |
- B) Complete data
|
| 330 |
- C) A confidence threshold
|
|
|
|
| 336 |
|
| 337 |
### Question 7
|
| 338 |
What is a "confidence threshold"?
|
|
|
|
| 339 |
- A) How sure you are about your method choice
|
| 340 |
- B) The minimum acceptable certainty for a result
|
| 341 |
- C) The maximum number of iterations
|
|
|
|
| 347 |
|
| 348 |
### Question 8
|
| 349 |
How do lecture notes differ from the textbook?
|
|
|
|
| 350 |
- A) Lectures focus on theory, textbook on application
|
| 351 |
- B) Lectures focus on application, textbook on theory
|
| 352 |
- C) They cover different topics entirely
|
|
|
|
| 358 |
|
| 359 |
### Question 9
|
| 360 |
What is a "base case" in this context?
|
|
|
|
| 361 |
- A) The most complex problem instance
|
| 362 |
- B) The simplest problem instance used as a starting point
|
| 363 |
- C) The final validated result
|
|
|
|
| 369 |
|
| 370 |
### Question 10
|
| 371 |
The three-stage process applies to which methods?
|
|
|
|
| 372 |
- A) Only the direct method
|
| 373 |
- B) Only the iterative method
|
| 374 |
- C) Direct and iterative only
|
|
|
|
| 379 |
}
|
| 380 |
|
| 381 |
|
| 382 |
+
def generate_mock_artifact(artifact_type: str, **kwargs) -> Artifact:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
time.sleep(1.5)
|
| 384 |
|
| 385 |
if artifact_type == "conversation_summary":
|
|
|
|
| 401 |
content = ""
|
| 402 |
title = artifact_type
|
| 403 |
|
| 404 |
+
return Artifact(
|
| 405 |
+
id=str(uuid.uuid4()),
|
| 406 |
+
type=artifact_type,
|
| 407 |
+
title=title,
|
| 408 |
+
content=content,
|
| 409 |
+
audio_path=None,
|
| 410 |
+
created_at=datetime.now().isoformat(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{ui → pages}/__init__.py
RENAMED
|
File without changes
|
pages/artifacts.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Artifacts tab: Summary, Podcast, and Quiz generation with mock data."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from state import UserData, get_active_notebook, get_all_artifacts, get_latest_artifact
|
| 5 |
+
from mock_data import generate_mock_artifact
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _format_time(iso_str: str) -> str:
|
| 9 |
+
try:
|
| 10 |
+
dt = datetime.fromisoformat(iso_str)
|
| 11 |
+
return dt.strftime("%b %d at %H:%M")
|
| 12 |
+
except (ValueError, KeyError):
|
| 13 |
+
return ""
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _render_artifact_content(artifact) -> str:
|
| 17 |
+
"""Render a single artifact as HTML with metadata."""
|
| 18 |
+
time_str = _format_time(artifact.created_at)
|
| 19 |
+
html = (
|
| 20 |
+
f'<div style="padding:4px 0 8px 0; font-size:0.8rem; color:#707088;">'
|
| 21 |
+
f'Generated {time_str}</div>'
|
| 22 |
+
)
|
| 23 |
+
html += (
|
| 24 |
+
f'<div style="max-height:400px; overflow-y:auto; padding:16px; '
|
| 25 |
+
f'background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.08); '
|
| 26 |
+
f'border-radius:14px;">'
|
| 27 |
+
)
|
| 28 |
+
# Convert markdown to basic display (Gradio HTML will render markdown in gr.Markdown)
|
| 29 |
+
html += f'<div class="artifact-content">{artifact.content}</div>'
|
| 30 |
+
html += '</div>'
|
| 31 |
+
|
| 32 |
+
if artifact.type == "podcast" and not artifact.audio_path:
|
| 33 |
+
html += (
|
| 34 |
+
'<div style="display:flex; align-items:center; gap:10px; padding:12px 16px; '
|
| 35 |
+
'background:rgba(102,126,234,0.06); border:1px solid rgba(102,126,234,0.15); '
|
| 36 |
+
'border-radius:10px; margin-top:8px;">'
|
| 37 |
+
'<span style="font-size:1.3rem;">🔇</span>'
|
| 38 |
+
'<span style="font-size:0.85rem; color:#8888aa;">Audio player will appear here when TTS is connected.</span>'
|
| 39 |
+
'</div>'
|
| 40 |
+
)
|
| 41 |
+
return html
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _render_history(artifacts: list, label: str) -> str:
|
| 45 |
+
if len(artifacts) <= 1:
|
| 46 |
+
return ""
|
| 47 |
+
html = f'<details><summary style="cursor:pointer; color:#a0a0b8; font-size:0.85rem; margin-top:12px;">Previous {label} ({len(artifacts) - 1})</summary>'
|
| 48 |
+
for a in artifacts[1:]:
|
| 49 |
+
time_str = _format_time(a.created_at)
|
| 50 |
+
html += f'<div style="margin-top:12px; padding:12px; border:1px solid rgba(255,255,255,0.06); border-radius:10px;">'
|
| 51 |
+
html += f'<strong>{a.title}</strong> — {time_str}'
|
| 52 |
+
html += f'<div style="max-height:200px; overflow-y:auto; margin-top:8px; font-size:0.85rem;">{a.content}</div>'
|
| 53 |
+
html += '</div>'
|
| 54 |
+
html += '</details>'
|
| 55 |
+
return html
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# ── No-sources gate ──────────────────────────────────────────────────────────
|
| 59 |
+
|
| 60 |
+
def render_no_sources_gate(state: UserData) -> str:
|
| 61 |
+
nb = get_active_notebook(state)
|
| 62 |
+
if not nb or not nb.sources:
|
| 63 |
+
return (
|
| 64 |
+
'<div class="empty-state">'
|
| 65 |
+
'<div style="font-size:3rem; margin-bottom:16px;">🎯</div>'
|
| 66 |
+
'<h3>Add sources first</h3>'
|
| 67 |
+
'<p>Upload documents in the <strong>Sources</strong> tab to unlock '
|
| 68 |
+
'summary, quiz, and podcast generation.</p>'
|
| 69 |
+
'</div>'
|
| 70 |
+
)
|
| 71 |
+
return ""
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def has_sources(state: UserData) -> bool:
|
| 75 |
+
nb = get_active_notebook(state)
|
| 76 |
+
return nb is not None and len(nb.sources) > 0
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ── Conversation Summary ─────────────────────────────────────────────────────
|
| 80 |
+
|
| 81 |
+
def render_conv_summary_section(state: UserData) -> str:
|
| 82 |
+
nb = get_active_notebook(state)
|
| 83 |
+
if not nb:
|
| 84 |
+
return ""
|
| 85 |
+
if not nb.messages:
|
| 86 |
+
return (
|
| 87 |
+
'<div style="text-align:center; padding:30px 20px; color:#606078; '
|
| 88 |
+
'border:1px dashed rgba(255,255,255,0.08); border-radius:14px;">'
|
| 89 |
+
'<p style="margin:0;">No conversation yet. Start chatting in the <strong>Chat</strong> tab first.</p>'
|
| 90 |
+
'</div>'
|
| 91 |
+
)
|
| 92 |
+
summaries = get_all_artifacts(nb, "conversation_summary")
|
| 93 |
+
if not summaries:
|
| 94 |
+
return '<p style="color:#606078; text-align:center; padding:20px;">Click "Generate" to create a conversation summary.</p>'
|
| 95 |
+
html = _render_artifact_content(summaries[0])
|
| 96 |
+
html += _render_history(summaries, "conversation summaries")
|
| 97 |
+
return html
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def handle_gen_conv_summary(style: str, state: UserData) -> tuple[UserData, str]:
|
| 101 |
+
nb = get_active_notebook(state)
|
| 102 |
+
if not nb or not nb.messages:
|
| 103 |
+
return state, render_conv_summary_section(state)
|
| 104 |
+
artifact = generate_mock_artifact("conversation_summary", style=style or "detailed")
|
| 105 |
+
nb.artifacts.append(artifact)
|
| 106 |
+
return state, render_conv_summary_section(state)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ── Document Summary ─────────────────────────────────────────────────────────
|
| 110 |
+
|
| 111 |
+
def render_doc_summary_section(state: UserData) -> str:
|
| 112 |
+
nb = get_active_notebook(state)
|
| 113 |
+
if not nb:
|
| 114 |
+
return ""
|
| 115 |
+
summaries = get_all_artifacts(nb, "document_summary")
|
| 116 |
+
if not summaries:
|
| 117 |
+
return '<p style="color:#606078; text-align:center; padding:20px;">Click "Generate" to create a document summary.</p>'
|
| 118 |
+
html = _render_artifact_content(summaries[0])
|
| 119 |
+
html += _render_history(summaries, "document summaries")
|
| 120 |
+
return html
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def handle_gen_doc_summary(style: str, state: UserData) -> tuple[UserData, str]:
|
| 124 |
+
nb = get_active_notebook(state)
|
| 125 |
+
if not nb:
|
| 126 |
+
return state, render_doc_summary_section(state)
|
| 127 |
+
artifact = generate_mock_artifact("document_summary", style=style or "detailed")
|
| 128 |
+
nb.artifacts.append(artifact)
|
| 129 |
+
return state, render_doc_summary_section(state)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ── Podcast ──────────────────────────────────────────────────────────────────
|
| 133 |
+
|
| 134 |
+
def has_any_summary(state: UserData) -> bool:
|
| 135 |
+
nb = get_active_notebook(state)
|
| 136 |
+
if not nb:
|
| 137 |
+
return False
|
| 138 |
+
return (
|
| 139 |
+
get_latest_artifact(nb, "document_summary") is not None
|
| 140 |
+
or get_latest_artifact(nb, "conversation_summary") is not None
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def render_podcast_locked() -> str:
|
| 145 |
+
return (
|
| 146 |
+
'<div class="locked-state">'
|
| 147 |
+
'<div style="font-size:2.5rem; margin-bottom:16px;">🔒</div>'
|
| 148 |
+
'<h3 style="color:#a0a0b8; font-weight:600; margin-bottom:8px;">Summary Required</h3>'
|
| 149 |
+
'<p style="color:#707088; font-size:0.9rem; line-height:1.6;">'
|
| 150 |
+
'Generate a summary first in the <strong>Summary</strong> tab.<br>'
|
| 151 |
+
'The podcast is created from your summary to ensure accuracy.</p>'
|
| 152 |
+
'<div style="margin-top:20px; display:inline-flex; align-items:center; gap:8px; '
|
| 153 |
+
'padding:8px 18px; background:rgba(102,126,234,0.08); '
|
| 154 |
+
'border:1px solid rgba(102,126,234,0.15); border-radius:20px; '
|
| 155 |
+
'font-size:0.82rem; color:#8090d0;">'
|
| 156 |
+
'📝 Summary → 🎙️ Podcast</div>'
|
| 157 |
+
'</div>'
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def render_podcast_section(state: UserData) -> str:
|
| 162 |
+
nb = get_active_notebook(state)
|
| 163 |
+
if not nb:
|
| 164 |
+
return ""
|
| 165 |
+
|
| 166 |
+
if not has_any_summary(state):
|
| 167 |
+
return render_podcast_locked()
|
| 168 |
+
|
| 169 |
+
# Show which summary will be used
|
| 170 |
+
latest_doc = get_latest_artifact(nb, "document_summary")
|
| 171 |
+
latest_conv = get_latest_artifact(nb, "conversation_summary")
|
| 172 |
+
latest = latest_doc or latest_conv
|
| 173 |
+
sum_title = latest.title
|
| 174 |
+
sum_time = _format_time(latest.created_at)
|
| 175 |
+
|
| 176 |
+
html = (
|
| 177 |
+
f'<div style="display:flex; align-items:center; gap:12px; padding:12px 18px; '
|
| 178 |
+
f'background:rgba(34,197,94,0.06); border:1px solid rgba(34,197,94,0.15); '
|
| 179 |
+
f'border-radius:12px; margin-bottom:16px;">'
|
| 180 |
+
f'<span style="font-size:1.2rem;">📝</span>'
|
| 181 |
+
f'<div><span style="font-size:0.85rem; color:#a0b8a0;">Based on: </span>'
|
| 182 |
+
f'<strong style="color:#c0e0c0;">{sum_title}</strong>'
|
| 183 |
+
f'<span style="color:#708070; font-size:0.8rem;"> ({sum_time})</span></div>'
|
| 184 |
+
f'</div>'
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
podcasts = get_all_artifacts(nb, "podcast")
|
| 188 |
+
if podcasts:
|
| 189 |
+
html += _render_artifact_content(podcasts[0])
|
| 190 |
+
html += _render_history(podcasts, "podcasts")
|
| 191 |
+
else:
|
| 192 |
+
html += (
|
| 193 |
+
'<div style="text-align:center; padding:40px 20px; color:#606078; margin-top:16px; '
|
| 194 |
+
'border:1px dashed rgba(255,255,255,0.08); border-radius:14px;">'
|
| 195 |
+
'<div style="font-size:2rem; margin-bottom:10px;">🎙️</div>'
|
| 196 |
+
'<p>No podcast generated yet.<br>Click <strong>Generate Podcast</strong> to create one.</p>'
|
| 197 |
+
'</div>'
|
| 198 |
+
)
|
| 199 |
+
return html
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def handle_gen_podcast(state: UserData) -> tuple[UserData, str]:
|
| 203 |
+
nb = get_active_notebook(state)
|
| 204 |
+
if not nb or not has_any_summary(state):
|
| 205 |
+
return state, render_podcast_section(state)
|
| 206 |
+
artifact = generate_mock_artifact("podcast")
|
| 207 |
+
nb.artifacts.append(artifact)
|
| 208 |
+
return state, render_podcast_section(state)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ── Quiz ─────────────────────────────────────────────────────────────────────
|
| 212 |
+
|
| 213 |
+
def render_quiz_section(state: UserData) -> str:
|
| 214 |
+
nb = get_active_notebook(state)
|
| 215 |
+
if not nb:
|
| 216 |
+
return ""
|
| 217 |
+
quizzes = get_all_artifacts(nb, "quiz")
|
| 218 |
+
if not quizzes:
|
| 219 |
+
return (
|
| 220 |
+
'<div style="text-align:center; padding:40px 20px; color:#606078; margin-top:16px; '
|
| 221 |
+
'border:1px dashed rgba(255,255,255,0.08); border-radius:14px;">'
|
| 222 |
+
'<div style="font-size:2rem; margin-bottom:10px;">❓</div>'
|
| 223 |
+
'<p>No quiz generated yet.<br>Choose the number of questions and click <strong>Generate Quiz</strong>.</p>'
|
| 224 |
+
'</div>'
|
| 225 |
+
)
|
| 226 |
+
html = _render_artifact_content(quizzes[0])
|
| 227 |
+
html += _render_history(quizzes, "quizzes")
|
| 228 |
+
return html
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def handle_gen_quiz(num_questions: int, state: UserData) -> tuple[UserData, str]:
|
| 232 |
+
nb = get_active_notebook(state)
|
| 233 |
+
if not nb:
|
| 234 |
+
return state, render_quiz_section(state)
|
| 235 |
+
num_q = int(num_questions) if num_questions else 5
|
| 236 |
+
artifact = generate_mock_artifact("quiz", num_questions=num_q)
|
| 237 |
+
nb.artifacts.append(artifact)
|
| 238 |
+
return state, render_quiz_section(state)
|
pages/chat.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chat tab: message display with citations and mock RAG responses."""
|
| 2 |
+
|
| 3 |
+
import uuid
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from state import UserData, Message, get_active_notebook
|
| 6 |
+
from mock_data import get_mock_response
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
FILE_TYPE_ICONS = {
|
| 10 |
+
"pdf": "📕", "pptx": "📊", "txt": "📝", "url": "🌐", "youtube": "🎬",
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def format_chatbot_messages(state: UserData) -> list[dict]:
|
| 15 |
+
"""Convert notebook messages to gr.Chatbot format with embedded citations."""
|
| 16 |
+
nb = get_active_notebook(state)
|
| 17 |
+
if not nb or not nb.messages:
|
| 18 |
+
return []
|
| 19 |
+
|
| 20 |
+
formatted = []
|
| 21 |
+
for msg in nb.messages:
|
| 22 |
+
content = msg.content
|
| 23 |
+
if msg.role == "assistant" and msg.citations:
|
| 24 |
+
# Add citation chips
|
| 25 |
+
chips = ""
|
| 26 |
+
for c in msg.citations:
|
| 27 |
+
chips += f'<span class="citation-chip">📄 {c["source"]} · p.{c["page"]}</span>'
|
| 28 |
+
content += f"\n\n{chips}"
|
| 29 |
+
# Add expandable passages
|
| 30 |
+
passages = ""
|
| 31 |
+
for c in msg.citations:
|
| 32 |
+
passages += f'> *"{c["text"]}"*\n>\n> — **{c["source"]}**, page {c["page"]}\n\n'
|
| 33 |
+
content += f"\n\n<details><summary>View cited passages</summary>\n\n{passages}</details>"
|
| 34 |
+
formatted.append({"role": msg.role, "content": content})
|
| 35 |
+
return formatted
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def render_no_sources_warning(state: UserData) -> str:
|
| 39 |
+
nb = get_active_notebook(state)
|
| 40 |
+
if not nb or len(nb.sources) == 0:
|
| 41 |
+
return (
|
| 42 |
+
'<div style="padding:14px 20px; background:rgba(234,179,8,0.08); '
|
| 43 |
+
'border:1px solid rgba(234,179,8,0.2); border-radius:12px; color:#d4a017; '
|
| 44 |
+
'font-size:0.9rem; margin-bottom:16px;">'
|
| 45 |
+
'Upload sources in the <strong>Sources</strong> tab to start chatting with your documents.'
|
| 46 |
+
'</div>'
|
| 47 |
+
)
|
| 48 |
+
return ""
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def handle_chat_submit(message: str, state: UserData) -> tuple[UserData, list[dict], str, str]:
|
| 52 |
+
"""Handle user sending a chat message. Returns (state, chatbot_messages, textbox_value, warning_html)."""
|
| 53 |
+
if not message or not message.strip():
|
| 54 |
+
return state, format_chatbot_messages(state), "", render_no_sources_warning(state)
|
| 55 |
+
|
| 56 |
+
nb = get_active_notebook(state)
|
| 57 |
+
if not nb:
|
| 58 |
+
return state, [], "", ""
|
| 59 |
+
|
| 60 |
+
# Add user message
|
| 61 |
+
user_msg = Message(
|
| 62 |
+
id=str(uuid.uuid4()),
|
| 63 |
+
role="user",
|
| 64 |
+
content=message.strip(),
|
| 65 |
+
citations=[],
|
| 66 |
+
created_at=datetime.now().isoformat(),
|
| 67 |
+
)
|
| 68 |
+
nb.messages.append(user_msg)
|
| 69 |
+
|
| 70 |
+
# Get mock response
|
| 71 |
+
response = get_mock_response(message)
|
| 72 |
+
|
| 73 |
+
# Add assistant message
|
| 74 |
+
assistant_msg = Message(
|
| 75 |
+
id=str(uuid.uuid4()),
|
| 76 |
+
role="assistant",
|
| 77 |
+
content=response["content"],
|
| 78 |
+
citations=response["citations"],
|
| 79 |
+
created_at=datetime.now().isoformat(),
|
| 80 |
+
)
|
| 81 |
+
nb.messages.append(assistant_msg)
|
| 82 |
+
|
| 83 |
+
return state, format_chatbot_messages(state), "", render_no_sources_warning(state)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def handle_clear_chat(state: UserData) -> tuple[UserData, list[dict], str]:
|
| 87 |
+
"""Clear all messages from the active notebook."""
|
| 88 |
+
nb = get_active_notebook(state)
|
| 89 |
+
if nb:
|
| 90 |
+
nb.messages = []
|
| 91 |
+
return state, [], render_no_sources_warning(state)
|
pages/sources.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sources tab: file upload, URL input, and source list management."""
|
| 2 |
+
|
| 3 |
+
import uuid
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from state import UserData, Source, get_active_notebook
|
| 6 |
+
|
| 7 |
+
ALLOWED_TYPES = ["pdf", "pptx", "txt"]
|
| 8 |
+
MAX_FILE_SIZE_MB = 15
|
| 9 |
+
MAX_SOURCES_PER_NOTEBOOK = 20
|
| 10 |
+
|
| 11 |
+
FILE_TYPE_CONFIG = {
|
| 12 |
+
"pdf": {"icon": "📕", "color": "239,68,68", "label": "PDF"},
|
| 13 |
+
"pptx": {"icon": "📊", "color": "249,115,22", "label": "PPTX"},
|
| 14 |
+
"txt": {"icon": "📝", "color": "59,130,246", "label": "TXT"},
|
| 15 |
+
"url": {"icon": "🌐", "color": "34,197,94", "label": "URL"},
|
| 16 |
+
"youtube": {"icon": "🎬", "color": "239,68,68", "label": "YouTube"},
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def render_source_header(state: UserData) -> str:
|
| 21 |
+
nb = get_active_notebook(state)
|
| 22 |
+
if not nb:
|
| 23 |
+
return ""
|
| 24 |
+
total = len(nb.sources)
|
| 25 |
+
remaining = MAX_SOURCES_PER_NOTEBOOK - total
|
| 26 |
+
return (
|
| 27 |
+
f'<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;">'
|
| 28 |
+
f'<div>'
|
| 29 |
+
f'<span style="font-size:1.1rem; font-weight:600; color:#e0e0f0;">Sources</span>'
|
| 30 |
+
f'<span style="margin-left:10px; padding:3px 10px; background:rgba(102,126,234,0.15); '
|
| 31 |
+
f'color:#8090d0; border-radius:12px; font-size:0.8rem; font-weight:600;">'
|
| 32 |
+
f'{total} / {MAX_SOURCES_PER_NOTEBOOK}</span>'
|
| 33 |
+
f'</div>'
|
| 34 |
+
f'<span style="font-size:0.8rem; color:#606078;">{remaining} slots remaining</span>'
|
| 35 |
+
f'</div>'
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def render_source_list(state: UserData) -> str:
|
| 40 |
+
nb = get_active_notebook(state)
|
| 41 |
+
if not nb or not nb.sources:
|
| 42 |
+
return (
|
| 43 |
+
'<div style="text-align:center; padding:50px 20px; color:#606078;">'
|
| 44 |
+
'<div style="font-size:3rem; margin-bottom:16px;">📄</div>'
|
| 45 |
+
'<h3 style="color:#a0a0b8; font-weight:600;">No sources yet</h3>'
|
| 46 |
+
'<p style="font-size:0.9rem;">Upload documents or add web links above.<br>'
|
| 47 |
+
'Your sources power the AI chat and artifact generation.</p>'
|
| 48 |
+
'</div>'
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
html = f'<p style="font-weight:600; font-size:0.9rem; color:#a0a0b8; margin-bottom:12px;">Your Sources ({len(nb.sources)})</p>'
|
| 52 |
+
for source in nb.sources:
|
| 53 |
+
ft = source.file_type
|
| 54 |
+
cfg = FILE_TYPE_CONFIG.get(ft, {"icon": "📄", "color": "150,150,170", "label": ft.upper()})
|
| 55 |
+
meta_parts = [cfg["label"]]
|
| 56 |
+
if source.size_mb:
|
| 57 |
+
meta_parts.append(f"{source.size_mb} MB")
|
| 58 |
+
if source.chunk_count > 0:
|
| 59 |
+
meta_parts.append(f"{source.chunk_count} chunks")
|
| 60 |
+
meta_str = " · ".join(meta_parts)
|
| 61 |
+
|
| 62 |
+
html += (
|
| 63 |
+
f'<div class="source-card">'
|
| 64 |
+
f'<div class="source-icon {ft}">{cfg["icon"]}</div>'
|
| 65 |
+
f'<div class="source-info">'
|
| 66 |
+
f'<div class="name">{source.filename}</div>'
|
| 67 |
+
f'<div class="meta">{meta_str}</div>'
|
| 68 |
+
f'</div>'
|
| 69 |
+
f'<span class="source-badge ready">Ready</span>'
|
| 70 |
+
f'</div>'
|
| 71 |
+
)
|
| 72 |
+
return html
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_source_choices(state: UserData) -> list[str]:
|
| 76 |
+
nb = get_active_notebook(state)
|
| 77 |
+
if not nb:
|
| 78 |
+
return []
|
| 79 |
+
return [s.filename for s in nb.sources]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def handle_file_upload(files, state: UserData) -> tuple[UserData, str, str, list[str]]:
|
| 83 |
+
"""Handle file upload. Returns (state, source_list_html, header_html, source_choices)."""
|
| 84 |
+
nb = get_active_notebook(state)
|
| 85 |
+
if not nb or not files:
|
| 86 |
+
return state, render_source_list(state), render_source_header(state), get_source_choices(state)
|
| 87 |
+
|
| 88 |
+
for f in files:
|
| 89 |
+
filename = f.name if hasattr(f, 'name') else str(f).rsplit("/", 1)[-1]
|
| 90 |
+
# Extract just the filename from the path
|
| 91 |
+
filename = filename.rsplit("/", 1)[-1] if "/" in filename else filename
|
| 92 |
+
|
| 93 |
+
existing_names = [s.filename for s in nb.sources]
|
| 94 |
+
if filename in existing_names:
|
| 95 |
+
continue
|
| 96 |
+
if len(nb.sources) >= MAX_SOURCES_PER_NOTEBOOK:
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
file_ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
|
| 100 |
+
if file_ext not in ALLOWED_TYPES:
|
| 101 |
+
continue
|
| 102 |
+
|
| 103 |
+
# Get file size
|
| 104 |
+
try:
|
| 105 |
+
import os
|
| 106 |
+
file_path = f.name if hasattr(f, 'name') else str(f)
|
| 107 |
+
size_bytes = os.path.getsize(file_path)
|
| 108 |
+
size_mb = round(size_bytes / (1024 * 1024), 2)
|
| 109 |
+
except Exception:
|
| 110 |
+
size_mb = 0
|
| 111 |
+
|
| 112 |
+
if size_mb > MAX_FILE_SIZE_MB:
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
source = Source(
|
| 116 |
+
id=str(uuid.uuid4()),
|
| 117 |
+
filename=filename,
|
| 118 |
+
file_type=file_ext,
|
| 119 |
+
size_mb=size_mb,
|
| 120 |
+
source_url=None,
|
| 121 |
+
chunk_count=0,
|
| 122 |
+
status="ready",
|
| 123 |
+
error_message=None,
|
| 124 |
+
created_at=datetime.now().isoformat(),
|
| 125 |
+
)
|
| 126 |
+
nb.sources.append(source)
|
| 127 |
+
|
| 128 |
+
return state, render_source_list(state), render_source_header(state), get_source_choices(state)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def handle_url_add(url: str, state: UserData) -> tuple[UserData, str, str, str, list[str]]:
|
| 132 |
+
"""Handle adding a URL source. Returns (state, source_list_html, header_html, url_textbox_value, source_choices)."""
|
| 133 |
+
nb = get_active_notebook(state)
|
| 134 |
+
if not nb or not url or not url.strip():
|
| 135 |
+
return state, render_source_list(state), render_source_header(state), "", get_source_choices(state)
|
| 136 |
+
|
| 137 |
+
url = url.strip()
|
| 138 |
+
if len(nb.sources) >= MAX_SOURCES_PER_NOTEBOOK:
|
| 139 |
+
return state, render_source_list(state), render_source_header(state), "", get_source_choices(state)
|
| 140 |
+
|
| 141 |
+
existing_urls = [s.source_url for s in nb.sources if s.source_url]
|
| 142 |
+
if url in existing_urls:
|
| 143 |
+
return state, render_source_list(state), render_source_header(state), "", get_source_choices(state)
|
| 144 |
+
|
| 145 |
+
is_youtube = "youtube.com" in url or "youtu.be" in url
|
| 146 |
+
file_type = "youtube" if is_youtube else "url"
|
| 147 |
+
display_name = url[:55] + "..." if len(url) > 55 else url
|
| 148 |
+
|
| 149 |
+
source = Source(
|
| 150 |
+
id=str(uuid.uuid4()),
|
| 151 |
+
filename=display_name,
|
| 152 |
+
file_type=file_type,
|
| 153 |
+
size_mb=None,
|
| 154 |
+
source_url=url,
|
| 155 |
+
chunk_count=0,
|
| 156 |
+
status="ready",
|
| 157 |
+
error_message=None,
|
| 158 |
+
created_at=datetime.now().isoformat(),
|
| 159 |
+
)
|
| 160 |
+
nb.sources.append(source)
|
| 161 |
+
|
| 162 |
+
return state, render_source_list(state), render_source_header(state), "", get_source_choices(state)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def handle_source_delete(source_name: str, state: UserData) -> tuple[UserData, str, str, list[str]]:
|
| 166 |
+
"""Delete a source by filename. Returns (state, source_list_html, header_html, source_choices)."""
|
| 167 |
+
nb = get_active_notebook(state)
|
| 168 |
+
if not nb or not source_name:
|
| 169 |
+
return state, render_source_list(state), render_source_header(state), get_source_choices(state)
|
| 170 |
+
|
| 171 |
+
nb.sources = [s for s in nb.sources if s.filename != source_name]
|
| 172 |
+
return state, render_source_list(state), render_source_header(state), get_source_choices(state)
|
requirements.txt
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
-
|
| 2 |
-
authlib>=1.3.2
|
|
|
|
| 1 |
+
gradio>=5.0.0
|
|
|
state.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Per-user state management with dataclasses and CRUD helpers."""
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import uuid
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class Source:
|
| 10 |
+
id: str
|
| 11 |
+
filename: str
|
| 12 |
+
file_type: str # "pdf", "pptx", "txt", "url", "youtube"
|
| 13 |
+
size_mb: float | None
|
| 14 |
+
source_url: str | None
|
| 15 |
+
chunk_count: int
|
| 16 |
+
status: str # "ready", "processing", "failed"
|
| 17 |
+
error_message: str | None
|
| 18 |
+
created_at: str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Message:
|
| 23 |
+
id: str
|
| 24 |
+
role: str # "user" or "assistant"
|
| 25 |
+
content: str
|
| 26 |
+
citations: list[dict] # [{source, page, text}]
|
| 27 |
+
created_at: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class Artifact:
|
| 32 |
+
id: str
|
| 33 |
+
type: str # "conversation_summary", "document_summary", "podcast", "quiz"
|
| 34 |
+
title: str
|
| 35 |
+
content: str
|
| 36 |
+
audio_path: str | None
|
| 37 |
+
created_at: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass
|
| 41 |
+
class Notebook:
|
| 42 |
+
id: str
|
| 43 |
+
title: str
|
| 44 |
+
created_at: str
|
| 45 |
+
sources: list[Source] = field(default_factory=list)
|
| 46 |
+
messages: list[Message] = field(default_factory=list)
|
| 47 |
+
artifacts: list[Artifact] = field(default_factory=list)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class UserData:
|
| 52 |
+
user_id: str
|
| 53 |
+
user_name: str
|
| 54 |
+
notebooks: dict[str, Notebook] = field(default_factory=dict)
|
| 55 |
+
active_notebook_id: str | None = None
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def create_default_user_data(user_id: str, user_name: str) -> UserData:
|
| 59 |
+
nb_id = str(uuid.uuid4())
|
| 60 |
+
default_nb = Notebook(
|
| 61 |
+
id=nb_id,
|
| 62 |
+
title="My First Notebook",
|
| 63 |
+
created_at=datetime.now().isoformat(),
|
| 64 |
+
)
|
| 65 |
+
return UserData(
|
| 66 |
+
user_id=user_id,
|
| 67 |
+
user_name=user_name,
|
| 68 |
+
notebooks={nb_id: default_nb},
|
| 69 |
+
active_notebook_id=nb_id,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_active_notebook(state: UserData) -> Notebook | None:
|
| 74 |
+
if state and state.active_notebook_id and state.active_notebook_id in state.notebooks:
|
| 75 |
+
return state.notebooks[state.active_notebook_id]
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def create_notebook(state: UserData, title: str) -> UserData:
|
| 80 |
+
nb_id = str(uuid.uuid4())
|
| 81 |
+
state.notebooks[nb_id] = Notebook(
|
| 82 |
+
id=nb_id,
|
| 83 |
+
title=title,
|
| 84 |
+
created_at=datetime.now().isoformat(),
|
| 85 |
+
)
|
| 86 |
+
state.active_notebook_id = nb_id
|
| 87 |
+
return state
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def delete_notebook(state: UserData, nb_id: str) -> UserData:
|
| 91 |
+
if nb_id in state.notebooks:
|
| 92 |
+
del state.notebooks[nb_id]
|
| 93 |
+
remaining = list(state.notebooks.keys())
|
| 94 |
+
state.active_notebook_id = remaining[0] if remaining else None
|
| 95 |
+
return state
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def rename_notebook(state: UserData, nb_id: str, new_title: str) -> UserData:
|
| 99 |
+
if nb_id in state.notebooks:
|
| 100 |
+
state.notebooks[nb_id].title = new_title
|
| 101 |
+
return state
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_notebook_choices(state: UserData) -> list[tuple[str, str]]:
|
| 105 |
+
"""Return list of (display_label, notebook_id) for gr.Radio."""
|
| 106 |
+
choices = []
|
| 107 |
+
for nb_id, nb in state.notebooks.items():
|
| 108 |
+
src_count = len(nb.sources)
|
| 109 |
+
msg_count = len(nb.messages)
|
| 110 |
+
label = nb.title
|
| 111 |
+
if src_count > 0 or msg_count > 0:
|
| 112 |
+
label += f" ({src_count}s, {msg_count}m)"
|
| 113 |
+
choices.append((label, nb_id))
|
| 114 |
+
return choices
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def get_all_artifacts(notebook: Notebook, artifact_type: str) -> list[Artifact]:
|
| 118 |
+
return [a for a in reversed(notebook.artifacts) if a.type == artifact_type]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def get_latest_artifact(notebook: Notebook, artifact_type: str) -> Artifact | None:
|
| 122 |
+
for artifact in reversed(notebook.artifacts):
|
| 123 |
+
if artifact.type == artifact_type:
|
| 124 |
+
return artifact
|
| 125 |
+
return None
|
theme.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dark theme, custom CSS, and logo SVGs for the NotebookLM Gradio app."""
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import gradio as gr
|
| 5 |
+
|
| 6 |
+
# ── Logo SVGs ────────────────────────────────────────────────────────────────
|
| 7 |
+
|
| 8 |
+
LOGO_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 60">
|
| 9 |
+
<defs>
|
| 10 |
+
<linearGradient id="lg1" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 11 |
+
<stop offset="0%" style="stop-color:#667eea"/>
|
| 12 |
+
<stop offset="100%" style="stop-color:#764ba2"/>
|
| 13 |
+
</linearGradient>
|
| 14 |
+
<linearGradient id="lg2" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 15 |
+
<stop offset="0%" style="stop-color:#a78bfa"/>
|
| 16 |
+
<stop offset="100%" style="stop-color:#667eea"/>
|
| 17 |
+
</linearGradient>
|
| 18 |
+
<linearGradient id="sp" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 19 |
+
<stop offset="0%" style="stop-color:#fbbf24"/>
|
| 20 |
+
<stop offset="100%" style="stop-color:#f59e0b"/>
|
| 21 |
+
</linearGradient>
|
| 22 |
+
</defs>
|
| 23 |
+
<g transform="translate(4,6)">
|
| 24 |
+
<rect x="2" y="4" width="36" height="44" rx="4" fill="url(#lg1)"/>
|
| 25 |
+
<rect x="2" y="4" width="8" height="44" rx="3" fill="url(#lg2)" opacity="0.7"/>
|
| 26 |
+
<line x1="16" y1="16" x2="32" y2="16" stroke="rgba(255,255,255,0.5)" stroke-width="1.8" stroke-linecap="round"/>
|
| 27 |
+
<line x1="16" y1="23" x2="30" y2="23" stroke="rgba(255,255,255,0.4)" stroke-width="1.8" stroke-linecap="round"/>
|
| 28 |
+
<line x1="16" y1="30" x2="32" y2="30" stroke="rgba(255,255,255,0.5)" stroke-width="1.8" stroke-linecap="round"/>
|
| 29 |
+
<line x1="16" y1="37" x2="28" y2="37" stroke="rgba(255,255,255,0.4)" stroke-width="1.8" stroke-linecap="round"/>
|
| 30 |
+
<g transform="translate(32,2)">
|
| 31 |
+
<path d="M6 0 L7.5 4.5 L12 6 L7.5 7.5 L6 12 L4.5 7.5 L0 6 L4.5 4.5 Z" fill="url(#sp)"/>
|
| 32 |
+
<path d="M14 8 L14.8 10.2 L17 11 L14.8 11.8 L14 14 L13.2 11.8 L11 11 L13.2 10.2 Z" fill="#fbbf24" opacity="0.7"/>
|
| 33 |
+
</g>
|
| 34 |
+
</g>
|
| 35 |
+
<text x="56" y="28" font-family="Inter,-apple-system,sans-serif" font-size="22" font-weight="700">
|
| 36 |
+
<tspan fill="url(#lg1)">Notebook</tspan><tspan fill="#a78bfa" font-weight="800">LM</tspan>
|
| 37 |
+
</text>
|
| 38 |
+
<text x="57" y="46" font-family="Inter,-apple-system,sans-serif" font-size="10.5" fill="#8888aa" font-weight="400" letter-spacing="0.8">
|
| 39 |
+
AI-Powered Study Companion
|
| 40 |
+
</text>
|
| 41 |
+
</svg>"""
|
| 42 |
+
|
| 43 |
+
LOGO_ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 60">
|
| 44 |
+
<defs>
|
| 45 |
+
<linearGradient id="ig1" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 46 |
+
<stop offset="0%" style="stop-color:#667eea"/>
|
| 47 |
+
<stop offset="100%" style="stop-color:#764ba2"/>
|
| 48 |
+
</linearGradient>
|
| 49 |
+
<linearGradient id="ig2" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 50 |
+
<stop offset="0%" style="stop-color:#a78bfa"/>
|
| 51 |
+
<stop offset="100%" style="stop-color:#667eea"/>
|
| 52 |
+
</linearGradient>
|
| 53 |
+
<linearGradient id="isp" x1="0%" y1="0%" x2="100%" y2="100%">
|
| 54 |
+
<stop offset="0%" style="stop-color:#fbbf24"/>
|
| 55 |
+
<stop offset="100%" style="stop-color:#f59e0b"/>
|
| 56 |
+
</linearGradient>
|
| 57 |
+
</defs>
|
| 58 |
+
<g transform="translate(2,4)">
|
| 59 |
+
<rect x="2" y="4" width="36" height="44" rx="5" fill="url(#ig1)"/>
|
| 60 |
+
<rect x="2" y="4" width="9" height="44" rx="4" fill="url(#ig2)" opacity="0.7"/>
|
| 61 |
+
<line x1="16" y1="16" x2="32" y2="16" stroke="rgba(255,255,255,0.55)" stroke-width="2" stroke-linecap="round"/>
|
| 62 |
+
<line x1="16" y1="23" x2="30" y2="23" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round"/>
|
| 63 |
+
<line x1="16" y1="30" x2="32" y2="30" stroke="rgba(255,255,255,0.55)" stroke-width="2" stroke-linecap="round"/>
|
| 64 |
+
<line x1="16" y1="37" x2="28" y2="37" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round"/>
|
| 65 |
+
<g transform="translate(30,0)">
|
| 66 |
+
<path d="M7 0 L8.8 5.3 L14 7 L8.8 8.8 L7 14 L5.2 8.8 L0 7 L5.2 5.3 Z" fill="url(#isp)"/>
|
| 67 |
+
<path d="M16 9 L17 11.5 L19.5 12.5 L17 13.5 L16 16 L15 13.5 L12.5 12.5 L15 11.5 Z" fill="#fbbf24" opacity="0.7"/>
|
| 68 |
+
</g>
|
| 69 |
+
</g>
|
| 70 |
+
</svg>"""
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_logo_b64(svg_str: str) -> str:
|
| 74 |
+
return base64.b64encode(svg_str.encode()).decode()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
LOGO_B64 = get_logo_b64(LOGO_SVG)
|
| 78 |
+
ICON_B64 = get_logo_b64(LOGO_ICON_SVG)
|
| 79 |
+
|
| 80 |
+
# ── Gradio Dark Theme ────────────────────────────────────────────────────────
|
| 81 |
+
|
| 82 |
+
dark_theme = gr.themes.Base(
|
| 83 |
+
primary_hue=gr.themes.colors.indigo,
|
| 84 |
+
secondary_hue=gr.themes.colors.purple,
|
| 85 |
+
neutral_hue=gr.themes.colors.slate,
|
| 86 |
+
font=gr.themes.GoogleFont("Inter"),
|
| 87 |
+
).set(
|
| 88 |
+
body_background_fill="#0e1117",
|
| 89 |
+
body_background_fill_dark="#0e1117",
|
| 90 |
+
block_background_fill="rgba(255,255,255,0.02)",
|
| 91 |
+
block_background_fill_dark="rgba(255,255,255,0.02)",
|
| 92 |
+
block_border_color="rgba(255,255,255,0.08)",
|
| 93 |
+
block_border_color_dark="rgba(255,255,255,0.08)",
|
| 94 |
+
block_label_text_color="#a0a0b8",
|
| 95 |
+
block_label_text_color_dark="#a0a0b8",
|
| 96 |
+
block_title_text_color="#e0e0f0",
|
| 97 |
+
block_title_text_color_dark="#e0e0f0",
|
| 98 |
+
body_text_color="#c8c8d8",
|
| 99 |
+
body_text_color_dark="#c8c8d8",
|
| 100 |
+
button_primary_background_fill="linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
| 101 |
+
button_primary_background_fill_dark="linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
| 102 |
+
button_primary_text_color="white",
|
| 103 |
+
button_primary_text_color_dark="white",
|
| 104 |
+
button_secondary_background_fill="rgba(255,255,255,0.04)",
|
| 105 |
+
button_secondary_background_fill_dark="rgba(255,255,255,0.04)",
|
| 106 |
+
button_secondary_border_color="rgba(255,255,255,0.12)",
|
| 107 |
+
button_secondary_border_color_dark="rgba(255,255,255,0.12)",
|
| 108 |
+
button_secondary_text_color="#d0d0e0",
|
| 109 |
+
button_secondary_text_color_dark="#d0d0e0",
|
| 110 |
+
border_color_primary="rgba(255,255,255,0.08)",
|
| 111 |
+
border_color_primary_dark="rgba(255,255,255,0.08)",
|
| 112 |
+
input_background_fill="rgba(255,255,255,0.03)",
|
| 113 |
+
input_background_fill_dark="rgba(255,255,255,0.03)",
|
| 114 |
+
input_border_color="rgba(255,255,255,0.1)",
|
| 115 |
+
input_border_color_dark="rgba(255,255,255,0.1)",
|
| 116 |
+
shadow_drop="none",
|
| 117 |
+
shadow_drop_lg="none",
|
| 118 |
+
shadow_spread="none",
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# ── Custom CSS ───────────────────────────────────────────────────────────────
|
| 122 |
+
|
| 123 |
+
CUSTOM_CSS = """
|
| 124 |
+
/* ── Import Google Font ── */
|
| 125 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 126 |
+
|
| 127 |
+
/* ── Sidebar ── */
|
| 128 |
+
#sidebar {
|
| 129 |
+
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%) !important;
|
| 130 |
+
border-right: 1px solid rgba(255,255,255,0.06);
|
| 131 |
+
padding: 16px !important;
|
| 132 |
+
min-height: 100vh;
|
| 133 |
+
position: sticky;
|
| 134 |
+
top: 0;
|
| 135 |
+
align-self: flex-start;
|
| 136 |
+
max-height: 100vh;
|
| 137 |
+
overflow-y: auto;
|
| 138 |
+
border-radius: 0 !important;
|
| 139 |
+
}
|
| 140 |
+
#sidebar .gr-button-primary {
|
| 141 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 142 |
+
border: none !important;
|
| 143 |
+
border-radius: 10px !important;
|
| 144 |
+
}
|
| 145 |
+
#sidebar .gr-button-secondary {
|
| 146 |
+
background: rgba(255,255,255,0.05) !important;
|
| 147 |
+
border: 1px solid rgba(255,255,255,0.1) !important;
|
| 148 |
+
border-radius: 10px !important;
|
| 149 |
+
color: #d0d0e0 !important;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* ── Notebook selector radio ── */
|
| 153 |
+
#notebook-selector label {
|
| 154 |
+
border-radius: 10px !important;
|
| 155 |
+
padding: 8px 14px !important;
|
| 156 |
+
transition: all 0.2s ease !important;
|
| 157 |
+
font-size: 0.85rem !important;
|
| 158 |
+
}
|
| 159 |
+
#notebook-selector label.selected {
|
| 160 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 161 |
+
color: white !important;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/* ── Tab styling ── */
|
| 165 |
+
.tabs > .tab-nav > button {
|
| 166 |
+
border-radius: 10px !important;
|
| 167 |
+
padding: 10px 24px !important;
|
| 168 |
+
font-weight: 500 !important;
|
| 169 |
+
font-size: 0.9rem !important;
|
| 170 |
+
}
|
| 171 |
+
.tabs > .tab-nav > button.selected {
|
| 172 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 173 |
+
color: white !important;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* ── Chat ── */
|
| 177 |
+
#chatbot {
|
| 178 |
+
border-radius: 14px !important;
|
| 179 |
+
border: 1px solid rgba(255,255,255,0.08) !important;
|
| 180 |
+
}
|
| 181 |
+
#chatbot .message {
|
| 182 |
+
border-radius: 14px !important;
|
| 183 |
+
padding: 14px 18px !important;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* ── Cards ── */
|
| 187 |
+
.source-card {
|
| 188 |
+
display: flex;
|
| 189 |
+
align-items: center;
|
| 190 |
+
gap: 16px;
|
| 191 |
+
padding: 16px 20px;
|
| 192 |
+
background: rgba(255,255,255,0.02);
|
| 193 |
+
border: 1px solid rgba(255,255,255,0.08);
|
| 194 |
+
border-radius: 14px;
|
| 195 |
+
margin-bottom: 10px;
|
| 196 |
+
transition: all 0.2s ease;
|
| 197 |
+
}
|
| 198 |
+
.source-card:hover {
|
| 199 |
+
border-color: rgba(102,126,234,0.3);
|
| 200 |
+
background: rgba(255,255,255,0.04);
|
| 201 |
+
}
|
| 202 |
+
.source-icon {
|
| 203 |
+
width: 48px; height: 48px; border-radius: 12px;
|
| 204 |
+
display: flex; align-items: center; justify-content: center;
|
| 205 |
+
font-size: 1.5rem; flex-shrink: 0;
|
| 206 |
+
}
|
| 207 |
+
.source-icon.pdf { background: rgba(239,68,68,0.15); }
|
| 208 |
+
.source-icon.pptx { background: rgba(249,115,22,0.15); }
|
| 209 |
+
.source-icon.txt { background: rgba(59,130,246,0.15); }
|
| 210 |
+
.source-icon.url { background: rgba(34,197,94,0.15); }
|
| 211 |
+
.source-icon.youtube { background: rgba(239,68,68,0.15); }
|
| 212 |
+
.source-info { flex: 1; min-width: 0; }
|
| 213 |
+
.source-info .name {
|
| 214 |
+
font-weight: 600; font-size: 0.95rem; color: #e0e0f0;
|
| 215 |
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
| 216 |
+
}
|
| 217 |
+
.source-info .meta { font-size: 0.8rem; color: #707088; margin-top: 2px; }
|
| 218 |
+
.source-badge {
|
| 219 |
+
padding: 4px 12px; border-radius: 20px; font-size: 0.75rem;
|
| 220 |
+
font-weight: 600; letter-spacing: 0.3px;
|
| 221 |
+
}
|
| 222 |
+
.source-badge.ready { background: rgba(34,197,94,0.15); color: #22c55e; }
|
| 223 |
+
|
| 224 |
+
/* ── Welcome hero ── */
|
| 225 |
+
.welcome-hero {
|
| 226 |
+
text-align: center; padding: 80px 40px;
|
| 227 |
+
background: linear-gradient(135deg, rgba(102,126,234,0.08) 0%, rgba(118,75,162,0.08) 100%);
|
| 228 |
+
border-radius: 20px; border: 1px solid rgba(102,126,234,0.15); margin: 20px 0;
|
| 229 |
+
}
|
| 230 |
+
.welcome-hero h1 {
|
| 231 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 232 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 233 |
+
font-size: 2.5rem; font-weight: 700; margin-bottom: 12px;
|
| 234 |
+
}
|
| 235 |
+
.welcome-hero p { color: #9090a8; font-size: 1.1rem; line-height: 1.6; }
|
| 236 |
+
|
| 237 |
+
/* ─��� Empty state ── */
|
| 238 |
+
.empty-state {
|
| 239 |
+
text-align: center; padding: 60px 30px; color: #707088;
|
| 240 |
+
}
|
| 241 |
+
.empty-state h3 { color: #a0a0b8; margin-bottom: 8px; font-weight: 600; }
|
| 242 |
+
.empty-state p { font-size: 0.95rem; line-height: 1.5; }
|
| 243 |
+
|
| 244 |
+
/* ── Notebook header ── */
|
| 245 |
+
.notebook-header {
|
| 246 |
+
padding: 0 0 16px 0; margin-bottom: 16px;
|
| 247 |
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
| 248 |
+
}
|
| 249 |
+
.notebook-header h2 {
|
| 250 |
+
font-weight: 700; font-size: 1.5rem; margin: 0; color: #e8e8f8;
|
| 251 |
+
}
|
| 252 |
+
.notebook-header .meta { font-size: 0.85rem; color: #707088; margin-top: 4px; }
|
| 253 |
+
|
| 254 |
+
/* ── Citation chip ── */
|
| 255 |
+
.citation-chip {
|
| 256 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 257 |
+
padding: 6px 14px; background: rgba(102,126,234,0.1);
|
| 258 |
+
border: 1px solid rgba(102,126,234,0.2); border-radius: 20px;
|
| 259 |
+
font-size: 0.8rem; color: #a0b0f0; margin: 3px 4px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* ── Artifact section header ── */
|
| 263 |
+
.artifact-section-header {
|
| 264 |
+
display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
|
| 265 |
+
}
|
| 266 |
+
.artifact-section-icon {
|
| 267 |
+
width: 36px; height: 36px; border-radius: 10px;
|
| 268 |
+
display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/* ── Locked state ── */
|
| 272 |
+
.locked-state {
|
| 273 |
+
text-align: center; padding: 50px 30px;
|
| 274 |
+
background: rgba(255,255,255,0.02);
|
| 275 |
+
border: 1px solid rgba(255,255,255,0.06);
|
| 276 |
+
border-radius: 16px;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* ── File uploader ── */
|
| 280 |
+
.gr-file-upload {
|
| 281 |
+
border-radius: 14px !important;
|
| 282 |
+
border: 2px dashed rgba(102,126,234,0.3) !important;
|
| 283 |
+
background: rgba(102,126,234,0.03) !important;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
/* ── Primary button ── */
|
| 287 |
+
.gr-button-primary {
|
| 288 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 289 |
+
border: none !important; border-radius: 10px !important;
|
| 290 |
+
font-weight: 600 !important;
|
| 291 |
+
}
|
| 292 |
+
.gr-button-primary:hover {
|
| 293 |
+
opacity: 0.9 !important;
|
| 294 |
+
transform: translateY(-1px) !important;
|
| 295 |
+
box-shadow: 0 4px 15px rgba(102,126,234,0.3) !important;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
/* ── Scrollbar ── */
|
| 299 |
+
::-webkit-scrollbar { width: 6px; }
|
| 300 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 301 |
+
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
|
| 302 |
+
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
|
| 303 |
+
|
| 304 |
+
/* ── Hide Gradio footer ── */
|
| 305 |
+
footer { display: none !important; }
|
| 306 |
+
|
| 307 |
+
/* ── Auth gate ── */
|
| 308 |
+
#auth-gate { max-width: 500px; margin: 100px auto; }
|
| 309 |
+
"""
|
| 310 |
+
|
| 311 |
+
# ── Reusable HTML Templates ──────────────────────────────────────────────────
|
| 312 |
+
|
| 313 |
+
WELCOME_HTML = f"""
|
| 314 |
+
<div class="welcome-hero">
|
| 315 |
+
<img src="data:image/svg+xml;base64,{ICON_B64}" style="width:64px; margin-bottom:16px;" />
|
| 316 |
+
<h1>NotebookLM</h1>
|
| 317 |
+
<p>Your AI-powered study companion.<br>
|
| 318 |
+
Sign in with your Hugging Face account to get started.</p>
|
| 319 |
+
</div>
|
| 320 |
+
"""
|
| 321 |
+
|
| 322 |
+
NO_NOTEBOOKS_HTML = """
|
| 323 |
+
<div class="welcome-hero">
|
| 324 |
+
<h1>NotebookLM</h1>
|
| 325 |
+
<p>Create a notebook from the sidebar to get started.</p>
|
| 326 |
+
</div>
|
| 327 |
+
"""
|
| 328 |
+
|
| 329 |
+
SIDEBAR_LOGO_HTML = f"""
|
| 330 |
+
<div style="padding: 8px 0 4px 0;">
|
| 331 |
+
<img src="data:image/svg+xml;base64,{LOGO_B64}" style="width:100%; max-width:240px;" />
|
| 332 |
+
</div>
|
| 333 |
+
"""
|
ui/chat_page.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import uuid
|
| 3 |
-
from datetime import datetime
|
| 4 |
-
import random
|
| 5 |
-
import time
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
# ── Mock responses ───────────────────────────────────────────────────────────
|
| 9 |
-
MOCK_RESPONSES = [
|
| 10 |
-
{
|
| 11 |
-
"content": (
|
| 12 |
-
"Based on the uploaded sources, the key concept revolves around "
|
| 13 |
-
"the relationship between the variables discussed in Chapter 3. "
|
| 14 |
-
"The author emphasizes that understanding this foundation is critical "
|
| 15 |
-
"before moving to advanced topics."
|
| 16 |
-
),
|
| 17 |
-
"citations": [
|
| 18 |
-
{"source": "lecture_notes.pdf", "page": 3, "text": "the relationship between variables..."},
|
| 19 |
-
{"source": "textbook_ch3.pdf", "page": 42, "text": "understanding this foundation..."},
|
| 20 |
-
],
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
"content": (
|
| 24 |
-
"The sources indicate three main approaches to this problem:\n\n"
|
| 25 |
-
"1. **Direct method** — Apply the formula from Section 2.1\n"
|
| 26 |
-
"2. **Iterative approach** — Build up from base cases\n"
|
| 27 |
-
"3. **Approximation** — Use the simplified model when precision isn't critical\n\n"
|
| 28 |
-
"The textbook recommends starting with the direct method for beginners."
|
| 29 |
-
),
|
| 30 |
-
"citations": [
|
| 31 |
-
{"source": "textbook_ch2.pdf", "page": 15, "text": "direct method... apply the formula"},
|
| 32 |
-
],
|
| 33 |
-
},
|
| 34 |
-
{
|
| 35 |
-
"content": (
|
| 36 |
-
"I couldn't find specific information about that topic in your "
|
| 37 |
-
"uploaded sources. Try uploading additional materials that cover this "
|
| 38 |
-
"subject, or rephrase your question to relate more closely to the "
|
| 39 |
-
"content in your current sources."
|
| 40 |
-
),
|
| 41 |
-
"citations": [],
|
| 42 |
-
},
|
| 43 |
-
{
|
| 44 |
-
"content": (
|
| 45 |
-
"Great question! According to the lecture slides, this concept "
|
| 46 |
-
"was introduced in Week 5. The key takeaway is that the process involves "
|
| 47 |
-
"three stages: **initialization**, **processing**, and **validation**. "
|
| 48 |
-
"Each stage has specific requirements that must be met before proceeding."
|
| 49 |
-
),
|
| 50 |
-
"citations": [
|
| 51 |
-
{"source": "week5_slides.pptx", "page": 8, "text": "three stages: initialization..."},
|
| 52 |
-
{"source": "week5_slides.pptx", "page": 12, "text": "specific requirements..."},
|
| 53 |
-
],
|
| 54 |
-
},
|
| 55 |
-
]
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def get_mock_response(query: str) -> dict:
|
| 59 |
-
"""Simulate a RAG response. Will be replaced with actual RAG pipeline."""
|
| 60 |
-
time.sleep(1.2)
|
| 61 |
-
return random.choice(MOCK_RESPONSES)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def render_chat(notebook: dict):
|
| 65 |
-
"""Render the chat interface."""
|
| 66 |
-
|
| 67 |
-
source_count = len(notebook["sources"])
|
| 68 |
-
|
| 69 |
-
# ── No sources warning ──
|
| 70 |
-
if source_count == 0:
|
| 71 |
-
st.markdown(
|
| 72 |
-
"""
|
| 73 |
-
<div style="
|
| 74 |
-
padding: 14px 20px;
|
| 75 |
-
background: rgba(234,179,8,0.08);
|
| 76 |
-
border: 1px solid rgba(234,179,8,0.2);
|
| 77 |
-
border-radius: 12px;
|
| 78 |
-
color: #d4a017;
|
| 79 |
-
font-size: 0.9rem;
|
| 80 |
-
margin-bottom: 16px;
|
| 81 |
-
">
|
| 82 |
-
Upload sources in the <strong>Sources</strong> tab to start chatting with your documents.
|
| 83 |
-
</div>
|
| 84 |
-
""",
|
| 85 |
-
unsafe_allow_html=True,
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
# ── Chat history ──
|
| 89 |
-
chat_container = st.container(height=480)
|
| 90 |
-
|
| 91 |
-
with chat_container:
|
| 92 |
-
if not notebook["messages"]:
|
| 93 |
-
st.markdown(
|
| 94 |
-
"""
|
| 95 |
-
<div class="empty-state" style="padding: 80px 20px;">
|
| 96 |
-
<div style="font-size: 3rem; margin-bottom: 16px;">💬</div>
|
| 97 |
-
<h3>Start a conversation</h3>
|
| 98 |
-
<p>Ask questions about your uploaded sources.<br>
|
| 99 |
-
The AI will answer using only your documents<br>
|
| 100 |
-
and provide citations for every claim.</p>
|
| 101 |
-
</div>
|
| 102 |
-
""",
|
| 103 |
-
unsafe_allow_html=True,
|
| 104 |
-
)
|
| 105 |
-
else:
|
| 106 |
-
for msg in notebook["messages"]:
|
| 107 |
-
avatar = "🧑" if msg["role"] == "user" else "🤖"
|
| 108 |
-
with st.chat_message(msg["role"], avatar=avatar):
|
| 109 |
-
st.markdown(msg["content"])
|
| 110 |
-
|
| 111 |
-
# Citations
|
| 112 |
-
if msg["role"] == "assistant" and msg.get("citations"):
|
| 113 |
-
citations_html = ""
|
| 114 |
-
for cite in msg["citations"]:
|
| 115 |
-
citations_html += (
|
| 116 |
-
f'<span class="citation-chip">'
|
| 117 |
-
f'📄 {cite["source"]} · p.{cite["page"]}'
|
| 118 |
-
f'</span>'
|
| 119 |
-
)
|
| 120 |
-
st.markdown(
|
| 121 |
-
f'<div style="margin-top: 10px;">{citations_html}</div>',
|
| 122 |
-
unsafe_allow_html=True,
|
| 123 |
-
)
|
| 124 |
-
# Expandable full citation text
|
| 125 |
-
with st.expander("View cited passages"):
|
| 126 |
-
for cite in msg["citations"]:
|
| 127 |
-
st.markdown(
|
| 128 |
-
f'> *"{cite["text"]}"*\n>\n'
|
| 129 |
-
f'> — **{cite["source"]}**, page {cite["page"]}'
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
# ── Chat input ──
|
| 133 |
-
if prompt := st.chat_input(
|
| 134 |
-
"Ask a question about your sources...",
|
| 135 |
-
key=f"chat_input_{notebook['id']}",
|
| 136 |
-
):
|
| 137 |
-
user_msg = {
|
| 138 |
-
"id": str(uuid.uuid4()),
|
| 139 |
-
"role": "user",
|
| 140 |
-
"content": prompt,
|
| 141 |
-
"citations": [],
|
| 142 |
-
"created_at": datetime.now().isoformat(),
|
| 143 |
-
}
|
| 144 |
-
notebook["messages"].append(user_msg)
|
| 145 |
-
|
| 146 |
-
with st.spinner("Thinking..."):
|
| 147 |
-
response = get_mock_response(prompt)
|
| 148 |
-
|
| 149 |
-
assistant_msg = {
|
| 150 |
-
"id": str(uuid.uuid4()),
|
| 151 |
-
"role": "assistant",
|
| 152 |
-
"content": response["content"],
|
| 153 |
-
"citations": response["citations"],
|
| 154 |
-
"created_at": datetime.now().isoformat(),
|
| 155 |
-
}
|
| 156 |
-
notebook["messages"].append(assistant_msg)
|
| 157 |
-
st.rerun()
|
| 158 |
-
|
| 159 |
-
# ── Controls ──
|
| 160 |
-
if notebook["messages"]:
|
| 161 |
-
cols = st.columns([5, 1])
|
| 162 |
-
with cols[1]:
|
| 163 |
-
if st.button("Clear chat", use_container_width=True):
|
| 164 |
-
notebook["messages"] = []
|
| 165 |
-
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/upload_page.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import uuid
|
| 3 |
-
from datetime import datetime
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
ALLOWED_TYPES = ["pdf", "pptx", "txt"]
|
| 7 |
-
MAX_FILE_SIZE_MB = 15
|
| 8 |
-
MAX_SOURCES_PER_NOTEBOOK = 20
|
| 9 |
-
|
| 10 |
-
FILE_TYPE_CONFIG = {
|
| 11 |
-
"pdf": {"icon": "📕", "color": "239,68,68", "label": "PDF"},
|
| 12 |
-
"pptx": {"icon": "📊", "color": "249,115,22", "label": "PPTX"},
|
| 13 |
-
"txt": {"icon": "📝", "color": "59,130,246", "label": "TXT"},
|
| 14 |
-
"url": {"icon": "🌐", "color": "34,197,94", "label": "URL"},
|
| 15 |
-
"youtube": {"icon": "🎬", "color": "239,68,68", "label": "YouTube"},
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def render_sources(notebook: dict):
|
| 20 |
-
"""Render source upload and management."""
|
| 21 |
-
|
| 22 |
-
total = len(notebook["sources"])
|
| 23 |
-
remaining = MAX_SOURCES_PER_NOTEBOOK - total
|
| 24 |
-
|
| 25 |
-
# ── Header with count ──
|
| 26 |
-
st.markdown(
|
| 27 |
-
f"""
|
| 28 |
-
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;">
|
| 29 |
-
<div>
|
| 30 |
-
<span style="font-size:1.1rem; font-weight:600; color:#e0e0f0;">Sources</span>
|
| 31 |
-
<span style="
|
| 32 |
-
margin-left: 10px;
|
| 33 |
-
padding: 3px 10px;
|
| 34 |
-
background: rgba(102,126,234,0.15);
|
| 35 |
-
color: #8090d0;
|
| 36 |
-
border-radius: 12px;
|
| 37 |
-
font-size: 0.8rem;
|
| 38 |
-
font-weight: 600;
|
| 39 |
-
">{total} / {MAX_SOURCES_PER_NOTEBOOK}</span>
|
| 40 |
-
</div>
|
| 41 |
-
<span style="font-size:0.8rem; color:#606078;">{remaining} slots remaining</span>
|
| 42 |
-
</div>
|
| 43 |
-
""",
|
| 44 |
-
unsafe_allow_html=True,
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
# ── Upload section ──
|
| 48 |
-
col_upload, col_url = st.columns([1, 1], gap="large")
|
| 49 |
-
|
| 50 |
-
with col_upload:
|
| 51 |
-
st.markdown(
|
| 52 |
-
'<p style="font-weight:600; font-size:0.9rem; color:#b0b0c8; margin-bottom:8px;">'
|
| 53 |
-
"Upload Files</p>",
|
| 54 |
-
unsafe_allow_html=True,
|
| 55 |
-
)
|
| 56 |
-
uploaded_files = st.file_uploader(
|
| 57 |
-
"Drop files here",
|
| 58 |
-
type=ALLOWED_TYPES,
|
| 59 |
-
accept_multiple_files=True,
|
| 60 |
-
help=f"PDF, PPTX, TXT — max {MAX_FILE_SIZE_MB}MB each",
|
| 61 |
-
key=f"uploader_{notebook['id']}",
|
| 62 |
-
label_visibility="collapsed",
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
if uploaded_files:
|
| 66 |
-
for f in uploaded_files:
|
| 67 |
-
existing_names = [s["filename"] for s in notebook["sources"]]
|
| 68 |
-
if f.name in existing_names:
|
| 69 |
-
continue
|
| 70 |
-
if len(notebook["sources"]) >= MAX_SOURCES_PER_NOTEBOOK:
|
| 71 |
-
st.error(f"Limit of {MAX_SOURCES_PER_NOTEBOOK} sources reached.")
|
| 72 |
-
break
|
| 73 |
-
|
| 74 |
-
file_size_mb = f.size / (1024 * 1024)
|
| 75 |
-
if file_size_mb > MAX_FILE_SIZE_MB:
|
| 76 |
-
st.error(f"**{f.name}** is too large ({file_size_mb:.1f}MB).")
|
| 77 |
-
continue
|
| 78 |
-
|
| 79 |
-
source = {
|
| 80 |
-
"id": str(uuid.uuid4()),
|
| 81 |
-
"filename": f.name,
|
| 82 |
-
"file_type": f.name.rsplit(".", 1)[-1].lower(),
|
| 83 |
-
"size_mb": round(file_size_mb, 2),
|
| 84 |
-
"chunk_count": 0,
|
| 85 |
-
"status": "ready",
|
| 86 |
-
"error_message": None,
|
| 87 |
-
"created_at": datetime.now().isoformat(),
|
| 88 |
-
}
|
| 89 |
-
notebook["sources"].append(source)
|
| 90 |
-
st.toast(f"Added {f.name}", icon="✅")
|
| 91 |
-
|
| 92 |
-
with col_url:
|
| 93 |
-
st.markdown(
|
| 94 |
-
'<p style="font-weight:600; font-size:0.9rem; color:#b0b0c8; margin-bottom:8px;">'
|
| 95 |
-
"Add Web Source</p>",
|
| 96 |
-
unsafe_allow_html=True,
|
| 97 |
-
)
|
| 98 |
-
url_input = st.text_input(
|
| 99 |
-
"URL",
|
| 100 |
-
placeholder="https://example.com or YouTube link",
|
| 101 |
-
label_visibility="collapsed",
|
| 102 |
-
key=f"url_input_{notebook['id']}",
|
| 103 |
-
)
|
| 104 |
-
if st.button("Add URL", use_container_width=True, type="primary"):
|
| 105 |
-
if not url_input.strip():
|
| 106 |
-
st.warning("Enter a URL.")
|
| 107 |
-
elif len(notebook["sources"]) >= MAX_SOURCES_PER_NOTEBOOK:
|
| 108 |
-
st.error(f"Limit of {MAX_SOURCES_PER_NOTEBOOK} sources reached.")
|
| 109 |
-
else:
|
| 110 |
-
url = url_input.strip()
|
| 111 |
-
existing_urls = [s.get("source_url") for s in notebook["sources"]]
|
| 112 |
-
if url in existing_urls:
|
| 113 |
-
st.warning("Already added.")
|
| 114 |
-
else:
|
| 115 |
-
is_youtube = "youtube.com" in url or "youtu.be" in url
|
| 116 |
-
file_type = "youtube" if is_youtube else "url"
|
| 117 |
-
display_name = url[:55] + "..." if len(url) > 55 else url
|
| 118 |
-
|
| 119 |
-
source = {
|
| 120 |
-
"id": str(uuid.uuid4()),
|
| 121 |
-
"filename": display_name,
|
| 122 |
-
"file_type": file_type,
|
| 123 |
-
"size_mb": None,
|
| 124 |
-
"source_url": url,
|
| 125 |
-
"chunk_count": 0,
|
| 126 |
-
"status": "ready",
|
| 127 |
-
"error_message": None,
|
| 128 |
-
"created_at": datetime.now().isoformat(),
|
| 129 |
-
}
|
| 130 |
-
notebook["sources"].append(source)
|
| 131 |
-
st.toast(f"Added {file_type} source", icon="✅")
|
| 132 |
-
st.rerun()
|
| 133 |
-
|
| 134 |
-
# ── Source list ──
|
| 135 |
-
st.divider()
|
| 136 |
-
|
| 137 |
-
if not notebook["sources"]:
|
| 138 |
-
st.markdown(
|
| 139 |
-
"""
|
| 140 |
-
<div style="
|
| 141 |
-
text-align: center; padding: 50px 20px;
|
| 142 |
-
color: #606078;
|
| 143 |
-
">
|
| 144 |
-
<div style="font-size: 3rem; margin-bottom: 16px;">📄</div>
|
| 145 |
-
<h3 style="color: #a0a0b8; font-weight: 600;">No sources yet</h3>
|
| 146 |
-
<p style="font-size: 0.9rem;">Upload documents or add web links above.<br>
|
| 147 |
-
Your sources power the AI chat and artifact generation.</p>
|
| 148 |
-
</div>
|
| 149 |
-
""",
|
| 150 |
-
unsafe_allow_html=True,
|
| 151 |
-
)
|
| 152 |
-
return
|
| 153 |
-
|
| 154 |
-
st.markdown(
|
| 155 |
-
f'<p style="font-weight:600; font-size:0.9rem; color:#a0a0b8; margin-bottom:12px;">'
|
| 156 |
-
f'Your Sources ({len(notebook["sources"])})</p>',
|
| 157 |
-
unsafe_allow_html=True,
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
for i, source in enumerate(notebook["sources"]):
|
| 161 |
-
ft = source["file_type"]
|
| 162 |
-
cfg = FILE_TYPE_CONFIG.get(ft, {"icon": "📄", "color": "150,150,170", "label": ft.upper()})
|
| 163 |
-
|
| 164 |
-
meta_parts = [cfg["label"]]
|
| 165 |
-
if source.get("size_mb"):
|
| 166 |
-
meta_parts.append(f"{source['size_mb']} MB")
|
| 167 |
-
if source["chunk_count"] > 0:
|
| 168 |
-
meta_parts.append(f"{source['chunk_count']} chunks")
|
| 169 |
-
meta_str = " · ".join(meta_parts)
|
| 170 |
-
|
| 171 |
-
status = source["status"]
|
| 172 |
-
|
| 173 |
-
with st.container(border=True):
|
| 174 |
-
col_icon, col_info, col_status, col_del = st.columns([0.5, 4, 1.2, 0.8])
|
| 175 |
-
|
| 176 |
-
with col_icon:
|
| 177 |
-
st.markdown(
|
| 178 |
-
f'<div style="font-size:1.8rem; text-align:center; padding-top:4px;">'
|
| 179 |
-
f'{cfg["icon"]}</div>',
|
| 180 |
-
unsafe_allow_html=True,
|
| 181 |
-
)
|
| 182 |
-
|
| 183 |
-
with col_info:
|
| 184 |
-
st.markdown(f"**{source['filename']}**")
|
| 185 |
-
st.caption(meta_str)
|
| 186 |
-
|
| 187 |
-
with col_status:
|
| 188 |
-
if status == "ready":
|
| 189 |
-
st.success("Ready")
|
| 190 |
-
elif status == "processing":
|
| 191 |
-
st.warning("Processing")
|
| 192 |
-
elif status == "failed":
|
| 193 |
-
st.error("Failed")
|
| 194 |
-
|
| 195 |
-
with col_del:
|
| 196 |
-
st.markdown('<div style="padding-top:8px;"></div>', unsafe_allow_html=True)
|
| 197 |
-
if st.button("X", key=f"rm_{source['id']}", help="Remove source"):
|
| 198 |
-
notebook["sources"].pop(i)
|
| 199 |
-
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|