Skip to content

Commit 8064573

Browse files
authored
Merge pull request #109 from UMass-Rescue/rework-ui-flow
Rework UI flow
2 parents 25c6370 + 56a8e43 commit 8064573

47 files changed

Lines changed: 2288 additions & 653 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/chatbot/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def create_input_form(
2626
form_card = ui.card().classes(
2727
"w-full max-w-full min-w-0 text-sm "
2828
"bg-white ring-1 ring-zinc-200 rounded-2xl rounded-tl-none shadow-sm "
29-
"border-0 rb-form-wrapper"
29+
"border-0 rb-form-wrapper !p-0"
3030
)
3131
with form_card:
3232
form_generator = FormGenerator()

frontend/chatbot/tool_config.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,40 @@ def create_advanced_granite_prompt(user_query: str) -> list[dict[str, str]]:
285285
# Generate Dynamic Schema for the prompt
286286
tools_definitions = generate_tool_definitions()
287287

288+
from frontend.utils import get_active_case
289+
from nicegui import app
290+
291+
active_case = get_active_case()
292+
context_prefix = ""
293+
if active_case:
294+
context_prefix += f"ACTIVE CASE CONTEXT:\n- Case Number: {active_case.caseNumber}\n- Evidence Path: {active_case.evidencePath}\n"
295+
context_prefix += "- Use the evidence path as the default input directory/file path for all tools if not specified otherwise.\n"
296+
297+
pipeline_job_id = None
298+
try:
299+
pipeline_job_id = app.storage.user.get("pipeline_job_id")
300+
except Exception:
301+
pass
302+
303+
if pipeline_job_id:
304+
try:
305+
from frontend.database import get_job_db
306+
307+
job = get_job_db().get_job_by_uid_sync(pipeline_job_id)
308+
if job and job.response:
309+
from frontend.chatbot.multi_tool_handler import extract_output_path
310+
from rb.api.models import ResponseBody
311+
312+
response_body = job.response
313+
if not isinstance(response_body, ResponseBody):
314+
response_body = ResponseBody(**response_body)
315+
output_path = extract_output_path(response_body)
316+
if output_path:
317+
context_prefix += f"PIPELINED JOB CONTEXT:\n- Source Job ID: {pipeline_job_id}\n- Source Job Output Path: {output_path}\n"
318+
context_prefix += "- Use this output path as the input directory/file path for the next tool call if the user asks to analyze/pipeline/use the results of the previous job.\n"
319+
except Exception as e:
320+
logger.error("Error extracting pipeline job output path in prompt: %s", e)
321+
288322
# ==========================================
289323
# FEW-SHOT PROMPTING (The Secret Sauce)
290324
# ==========================================
@@ -293,6 +327,7 @@ def create_advanced_granite_prompt(user_query: str) -> list[dict[str, str]]:
293327
system_msg = {
294328
"role": "system",
295329
"content": (
330+
f"{context_prefix}\n"
296331
"You are a forensic analysis assistant for RescueBox.\n"
297332
"RULES:\n"
298333
"1. CHAINING: If the user requests multiple actions, generate a LIST of tools in execution order.\n"

frontend/components/about.py

Lines changed: 120 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
from pathlib import Path
44
from typing import List, Optional, Tuple
5-
from urllib.parse import quote, unquote
5+
from urllib.parse import unquote
66

77
from nicegui import ui
88
from starlette.requests import Request
@@ -16,7 +16,7 @@
1616
logger.setLevel(logging.INFO)
1717

1818

19-
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
19+
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
2020
LICENSE_ROOT = _REPO_ROOT / "License&Copyright"
2121
# Relative to LICENSE_ROOT — default document when ``?doc=`` is missing/invalid.
2222
DEFAULT_LICENSE_REL = "LICENSE"
@@ -114,7 +114,7 @@ def render_one_file(
114114
)
115115
ui.code(raw).classes(
116116
"w-full max-w-none text-sm whitespace-pre-wrap break-words "
117-
"block p-4 bg-zinc-50 rounded-lg border border-zinc-300"
117+
"block p-4 bg-slate-50 rounded-xl border border-slate-200 shadow-inner"
118118
)
119119

120120

@@ -124,92 +124,132 @@ def render_license_documents_section(
124124
static_url: str = "/license-copyright",
125125
page_path: str = "/about",
126126
) -> None:
127-
"""License & Copyright picker and viewer; uses ``?doc=`` on ``page_path``."""
128-
doc = request.query_params.get("doc")
127+
"""License & Copyright picker and viewer; uses dynamic, inline, closable, and scrollable rendering."""
129128
root = LICENSE_ROOT
130129
files = list_text_docs(root)
131130

132131
ui.element("div").props('id="license-copyright"').classes("scroll-mt-24")
133132
with ui.card().classes(
134-
"w-full max-w-3xl p-6 bg-white border border-zinc-300 rounded-xl shadow-sm"
133+
"w-full max-w-3xl p-6 bg-white border border-slate-200 rounded-2xl shadow-md border-t-4 border-t-[#881c1c] flex flex-col gap-4"
135134
):
136-
ui.label("License & Copyright").classes(
137-
"text-xl font-semibold text-[#505759] mb-2"
138-
)
135+
ui.label("License & Copyright").classes("text-xl font-semibold text-slate-800")
139136
ui.label(
140-
"RescueBox LICENSE, COPYRIGHT, and NOTICE, see bundled third-party notices when you choose Third party."
141-
).classes("text-sm text-zinc-600 mb-4")
142-
143-
if not root.is_dir():
144-
ui.label(f"Folder not found: {root}").classes("text-red-600")
145-
return
146-
if not files:
147-
ui.label("No license documents found in that folder.").classes("text-zinc-600")
148-
return
149-
150-
primary_entries, third_party_files = _primary_and_third_party_paths(files)
151-
152-
rel = unquote(doc) if doc else ""
153-
if rel not in files:
154-
if DEFAULT_LICENSE_REL in files:
155-
rel = DEFAULT_LICENSE_REL
156-
elif primary_entries:
157-
rel = primary_entries[0][1]
158-
else:
159-
rel = files[0]
160-
161-
base = page_path.rstrip("/") or "/about"
162-
163-
def _navigate_to_doc(new_rel: str) -> None:
164-
if new_rel in files:
165-
ui.navigate.to(f"{base}?doc={quote(new_rel, safe='')}")
137+
"Select a document below to view RescueBox LICENSE, COPYRIGHT, NOTICE, or bundled third-party notices."
138+
).classes("text-sm text-zinc-600")
166139

167-
# Main picker: primary docs + optional "Third party".
168-
# NiceGUI dict options are {value: label} (keys are selected values; values are shown in the UI).
169-
main_options: dict[str, str] = {path: label for label, path in primary_entries}
170-
if third_party_files:
171-
main_options[_THIRD_PARTY_SENTINEL] = "Third party"
172-
173-
if rel in third_party_files:
174-
main_value = _THIRD_PARTY_SENTINEL
175-
else:
176-
main_value = next(
177-
(path for label, path in primary_entries if path == rel),
178-
primary_entries[0][1] if primary_entries else rel,
179-
)
180-
181-
def _on_main_pick(e) -> None:
182-
v = e.value
183-
if not isinstance(v, str):
140+
if not root.is_dir():
141+
ui.label(f"Folder not found: {root}").classes("text-red-600")
184142
return
185-
if v == _THIRD_PARTY_SENTINEL and third_party_files:
186-
target = rel if rel in third_party_files else third_party_files[0]
187-
_navigate_to_doc(target)
188-
elif v != _THIRD_PARTY_SENTINEL:
189-
_navigate_to_doc(v)
190-
191-
ui.select(
192-
options=main_options,
193-
value=main_value,
194-
label="Document",
195-
on_change=_on_main_pick,
196-
).classes("w-full max-w-2xl")
197-
198-
if third_party_files:
199-
with ui.column().classes("w-full max-w-2xl mt-2") as third_wrap:
200-
third_wrap.visible = rel in third_party_files
201-
202-
def _on_third_pick(e) -> None:
203-
v = e.value
204-
if isinstance(v, str) and v in third_party_files:
205-
_navigate_to_doc(v)
206-
207-
ui.select(
143+
if not files:
144+
ui.label("No license documents found in that folder.").classes(
145+
"text-zinc-600"
146+
)
147+
return
148+
149+
primary_entries, third_party_files = _primary_and_third_party_paths(files)
150+
151+
# Main options for the select dropdown
152+
main_options: dict[str, str] = {path: label for label, path in primary_entries}
153+
if third_party_files:
154+
main_options[_THIRD_PARTY_SENTINEL] = "Third party"
155+
156+
# Dropdowns row
157+
with ui.row().classes("w-full gap-4 items-center flex-wrap sm:flex-nowrap"):
158+
# Primary document selector
159+
main_select = ui.select(
160+
options=main_options,
161+
label="Document",
162+
value=None,
163+
on_change=lambda e: _on_main_change(e),
164+
).classes("flex-1 min-w-[200px]")
165+
166+
# Third-party document selector (hidden by default)
167+
third_select = ui.select(
208168
options=third_party_files,
209-
value=rel if rel in third_party_files else third_party_files[0],
210169
label="Third-party document",
211-
on_change=_on_third_pick,
212-
).classes("w-full")
170+
value=None,
171+
on_change=lambda e: _on_third_change(e),
172+
).classes("flex-1 min-w-[200px]")
173+
third_select.visible = False
174+
175+
# Closable & Scrollable Viewer Container (hidden by default)
176+
with ui.card().classes(
177+
"w-full p-4 bg-slate-50 border border-slate-200 rounded-xl shadow-sm flex flex-col gap-3"
178+
) as viewer_card:
179+
viewer_card.visible = False
180+
181+
# Viewer Header
182+
with ui.row().classes(
183+
"w-full justify-between items-center border-b pb-2 border-slate-200"
184+
):
185+
with ui.row().classes("items-center gap-2"):
186+
ui.icon("article", size="sm").classes("text-[#881c1c]")
187+
viewer_title = ui.label("").classes(
188+
"text-sm font-bold text-slate-700 font-mono"
189+
)
190+
191+
# Close button
192+
ui.button(
193+
"Close",
194+
icon="close",
195+
color=None,
196+
on_click=lambda: _close_viewer(),
197+
).props("flat dense no-caps").classes(
198+
"text-slate-600 hover:text-slate-800 hover:bg-slate-100 px-3 py-1 "
199+
"rounded-lg border border-slate-200 transition-colors text-sm font-medium"
200+
)
201+
202+
# Scrollable body
203+
viewer_body = ui.column().classes(
204+
"w-full max-h-[350px] overflow-y-auto pr-2"
205+
)
213206

214-
body = ui.column().classes("w-full min-w-0 mt-6")
215-
render_one_file(body, root, rel, static_url=static_url)
207+
# Helper to render document content
208+
def _show_document(rel_path: str):
209+
viewer_body.clear()
210+
viewer_title.text = rel_path
211+
render_one_file(viewer_body, root, rel_path, static_url=static_url)
212+
viewer_card.visible = True
213+
214+
# Close viewer action
215+
def _close_viewer():
216+
viewer_card.visible = False
217+
main_select.value = None
218+
third_select.value = None
219+
third_select.visible = False
220+
221+
# On primary selection change
222+
def _on_main_change(e):
223+
val = e.value
224+
if not val:
225+
return
226+
if val == _THIRD_PARTY_SENTINEL:
227+
third_select.visible = True
228+
# Automatically select and show the first third party file
229+
first_third = third_party_files[0] if third_party_files else None
230+
if first_third:
231+
third_select.value = first_third
232+
_show_document(first_third)
233+
else:
234+
third_select.visible = False
235+
third_select.value = None
236+
_show_document(val)
237+
238+
# On third-party selection change
239+
def _on_third_change(e):
240+
val = e.value
241+
if val and val in third_party_files:
242+
_show_document(val)
243+
244+
# Initial load from query param if present
245+
doc = request.query_params.get("doc")
246+
if doc:
247+
rel = unquote(doc)
248+
if rel in files:
249+
if rel in third_party_files:
250+
main_select.value = _THIRD_PARTY_SENTINEL
251+
third_select.value = rel
252+
third_select.visible = True
253+
else:
254+
main_select.value = rel
255+
_show_document(rel)

frontend/components/chat/dialogs.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ def show_help_dialog(help_text: str, title: Optional[str] = "RescueBox Help") ->
88
with ui.dialog() as dialog, ui.card().classes(Design.PANEL_SHELL_CARD_WIDE):
99
with ui.row().classes(Design.PANEL_SHELL_HEADER):
1010
ui.label(title or "Help").classes(Design.PANEL_SHELL_HEADER_TITLE)
11-
ui.button(icon="close", on_click=dialog.close).props("flat round dense")
11+
ui.button(icon="close", color=None, on_click=dialog.close).props(
12+
"flat round dense"
13+
)
1214
with ui.column().classes("w-full flex-1 overflow-y-auto p-6"):
1315
ui.markdown(help_text or "No help available.")
1416
dialog.open()
@@ -26,7 +28,9 @@ async def show_history_dialog(
2628
with ui.dialog() as dialog, ui.card().classes(Design.PANEL_SHELL_CARD_WIDE):
2729
with ui.row().classes(Design.PANEL_SHELL_HEADER):
2830
ui.label("Chat History").classes(Design.PANEL_SHELL_HEADER_TITLE)
29-
ui.button(icon="close", on_click=dialog.close).props("flat round dense")
31+
ui.button(icon="close", color=None, on_click=dialog.close).props(
32+
"flat round dense"
33+
)
3034

3135
with ui.column().classes(
3236
f"{Design.PANEL_SHELL_BODY} gap-3 overflow-y-auto max-h-[60vh] w-full"
@@ -99,5 +103,7 @@ def _render_message_card(msg: Any) -> None:
99103
):
100104
for msg in messages:
101105
_render_message_card(msg)
102-
ui.button("Close", on_click=dialog.close).classes(Design.BTN_MEDIUM_GRAY)
106+
ui.button("Close", color=None, on_click=dialog.close).classes(
107+
Design.BTN_MEDIUM_GRAY
108+
)
103109
dialog.open()

0 commit comments

Comments
 (0)