spagestic commited on
Commit
35be521
·
1 Parent(s): 450f57c

Compact Chat Tool UI is implemented

Browse files
Files changed (4) hide show
  1. assets/app.js +121 -36
  2. assets/server.css +66 -6
  3. ui/agent/graph/respond.py +16 -9
  4. ui/agent/tools.py +40 -6
assets/app.js CHANGED
@@ -560,28 +560,121 @@ function appendToolLogSection(parent, label, value) {
560
  parent.appendChild(section);
561
  }
562
 
563
- async function renderToolMessage(node, message, metadata) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  const isThinking = metadata.title === "Thinking";
565
  if (isThinking) {
566
  node.classList.add("thinking");
567
  }
568
 
569
- if (metadata.log) {
 
 
 
 
 
 
 
570
  const details = document.createElement("details");
571
- details.className = "tool-log";
 
 
 
 
572
  const summary = document.createElement("summary");
573
- const statusSuffix =
574
- metadata.status === "pending"
575
- ? " (running…)"
576
- : metadata.duration
577
- ? ` (${metadata.duration.toFixed(1)}s)`
578
- : "";
579
- summary.textContent = `${metadata.title || "Tool"}${statusSuffix}`;
580
  details.appendChild(summary);
581
 
582
  if (message.content) {
583
  const summaryText = document.createElement("div");
584
- summaryText.className = "tool-summary-text";
585
  if (metadata.markdown) {
586
  summaryText.classList.add("markdown-body");
587
  await renderMarkdownInto(summaryText, message.content);
@@ -591,11 +684,17 @@ async function renderToolMessage(node, message, metadata) {
591
  details.appendChild(summaryText);
592
  }
593
 
594
- if (metadata.log.arguments) {
595
- appendToolLogSection(details, "Arguments", metadata.log.arguments);
596
- }
597
- if (metadata.log.result !== undefined) {
598
- appendToolLogSection(details, "Result", metadata.log.result);
 
 
 
 
 
 
599
  }
600
 
601
  node.appendChild(details);
@@ -642,19 +741,6 @@ function researchTaskKey(task) {
642
  return researchTaskDisplay(task).country || "research";
643
  }
644
 
645
- function shortResearchEventTitle(title, task) {
646
- const text = String(title || "").trim();
647
- const { country } = researchTaskDisplay(task);
648
- if (country && text.includes(` · ${country}`)) {
649
- return text.split(` · ${country}`)[0].trim();
650
- }
651
- const separator = text.indexOf(" · ");
652
- if (separator !== -1) {
653
- return text.slice(0, separator).trim();
654
- }
655
- return text || "Research update";
656
- }
657
-
658
  function collectResearchMessages() {
659
  return state.history.filter(isResearchMessage);
660
  }
@@ -716,13 +802,8 @@ async function renderResearchMessage(parent, message, task) {
716
  const metadata = message.metadata || {};
717
  node.className = `research-event ${metadata.status === "pending" ? "pending" : ""}`;
718
 
719
- const title = document.createElement("div");
720
- title.className = "research-event-title";
721
- title.textContent = shortResearchEventTitle(metadata.title, task);
722
- node.appendChild(title);
723
-
724
  if (metadata.log) {
725
- await renderToolMessage(node, message, metadata);
726
  } else {
727
  const body = document.createElement("div");
728
  await renderMessageBody(body, message, false);
@@ -878,7 +959,11 @@ async function renderMessages() {
878
  }
879
 
880
  if (isTool) {
881
- await renderToolMessage(node, message, metadata);
 
 
 
 
882
  } else {
883
  const body = document.createElement("div");
884
  await renderMessageBody(body, message, isTool);
 
560
  parent.appendChild(section);
561
  }
562
 
563
+ function appendToolRawLog(parent, metadata) {
564
+ const log = metadata.log || {};
565
+ const hasArguments = log.arguments !== undefined && log.arguments !== null;
566
+ const hasResult = log.result !== undefined && log.result !== null;
567
+ if (!hasArguments && !hasResult) {
568
+ return;
569
+ }
570
+
571
+ const rawDetails = document.createElement("details");
572
+ rawDetails.className = "tool-log-raw";
573
+ const rawSummary = document.createElement("summary");
574
+ rawSummary.textContent = "Technical details";
575
+ rawDetails.appendChild(rawSummary);
576
+
577
+ if (hasArguments) {
578
+ appendToolLogSection(rawDetails, "Arguments", log.arguments);
579
+ }
580
+ if (hasResult) {
581
+ appendToolLogSection(rawDetails, "Result", log.result);
582
+ }
583
+ parent.appendChild(rawDetails);
584
+ }
585
+
586
+ function toolSummarySuffix(metadata) {
587
+ if (metadata.status === "pending") {
588
+ return " (running…)";
589
+ }
590
+ if (metadata.duration) {
591
+ return ` (${metadata.duration.toFixed(1)}s)`;
592
+ }
593
+ return "";
594
+ }
595
+
596
+ function resolvePlanTodos(metadata) {
597
+ if (Array.isArray(metadata.plan_todos) && metadata.plan_todos.length) {
598
+ return metadata.plan_todos;
599
+ }
600
+ const legacy = metadata.log?.result;
601
+ return Array.isArray(legacy) ? legacy : [];
602
+ }
603
+
604
+ function renderPlanTodoList(parent, todos) {
605
+ const list = document.createElement("div");
606
+ list.className = "plan-todo-list";
607
+ for (const todo of todos) {
608
+ const item = document.createElement("div");
609
+ item.className = "plan-todo-item";
610
+
611
+ const country = document.createElement("div");
612
+ country.className = "plan-todo-country";
613
+ country.textContent = todo.country || todo.title || "Country";
614
+ item.appendChild(country);
615
+
616
+ const methods = document.createElement("div");
617
+ methods.className = "plan-todo-methods";
618
+ methods.textContent = todo.methods || todo.description || "";
619
+ if (methods.textContent) {
620
+ item.appendChild(methods);
621
+ }
622
+
623
+ list.appendChild(item);
624
+ }
625
+ parent.appendChild(list);
626
+ }
627
+
628
+ async function renderPlanMessage(node, message, metadata) {
629
+ const details = document.createElement("details");
630
+ details.className = "tool-log plan-message";
631
+ details.open = true;
632
+
633
+ const summary = document.createElement("summary");
634
+ summary.textContent = metadata.title || "Research plan";
635
+ details.appendChild(summary);
636
+
637
+ const todos = resolvePlanTodos(metadata);
638
+ if (todos.length) {
639
+ renderPlanTodoList(details, todos);
640
+ } else if (message.content) {
641
+ const fallback = document.createElement("div");
642
+ fallback.className = "tool-compact-detail";
643
+ fallback.textContent = message.content;
644
+ details.appendChild(fallback);
645
+ }
646
+
647
+ node.appendChild(details);
648
+ }
649
+
650
+ async function renderToolMessage(node, message, metadata, options = {}) {
651
  const isThinking = metadata.title === "Thinking";
652
  if (isThinking) {
653
  node.classList.add("thinking");
654
  }
655
 
656
+ if (metadata.display === "plan") {
657
+ await renderPlanMessage(node, message, metadata);
658
+ return;
659
+ }
660
+
661
+ const isCompact = Boolean(options.compact || metadata.compact);
662
+
663
+ if (metadata.log || isCompact) {
664
  const details = document.createElement("details");
665
+ details.className = `tool-log${isCompact ? " tool-log-compact" : ""}`;
666
+ if (isCompact) {
667
+ details.open = true;
668
+ }
669
+
670
  const summary = document.createElement("summary");
671
+ summary.className = "tool-compact-summary";
672
+ summary.textContent = `${metadata.title || "Tool"}${toolSummarySuffix(metadata)}`;
 
 
 
 
 
673
  details.appendChild(summary);
674
 
675
  if (message.content) {
676
  const summaryText = document.createElement("div");
677
+ summaryText.className = isCompact ? "tool-compact-detail" : "tool-summary-text";
678
  if (metadata.markdown) {
679
  summaryText.classList.add("markdown-body");
680
  await renderMarkdownInto(summaryText, message.content);
 
684
  details.appendChild(summaryText);
685
  }
686
 
687
+ if (metadata.log) {
688
+ if (isCompact) {
689
+ appendToolRawLog(details, metadata);
690
+ } else {
691
+ if (metadata.log.arguments) {
692
+ appendToolLogSection(details, "Arguments", metadata.log.arguments);
693
+ }
694
+ if (metadata.log.result !== undefined) {
695
+ appendToolLogSection(details, "Result", metadata.log.result);
696
+ }
697
+ }
698
  }
699
 
700
  node.appendChild(details);
 
741
  return researchTaskDisplay(task).country || "research";
742
  }
743
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  function collectResearchMessages() {
745
  return state.history.filter(isResearchMessage);
746
  }
 
802
  const metadata = message.metadata || {};
803
  node.className = `research-event ${metadata.status === "pending" ? "pending" : ""}`;
804
 
 
 
 
 
 
805
  if (metadata.log) {
806
+ await renderToolMessage(node, message, metadata, { compact: true });
807
  } else {
808
  const body = document.createElement("div");
809
  await renderMessageBody(body, message, false);
 
959
  }
960
 
961
  if (isTool) {
962
+ if (metadata.display === "plan") {
963
+ await renderPlanMessage(node, message, metadata);
964
+ } else {
965
+ await renderToolMessage(node, message, metadata);
966
+ }
967
  } else {
968
  const body = document.createElement("div");
969
  await renderMessageBody(body, message, isTool);
assets/server.css CHANGED
@@ -328,6 +328,62 @@ button:disabled {
328
  color: #e5e7eb;
329
  }
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  .markdown-body > :first-child {
332
  margin-top: 0;
333
  }
@@ -581,17 +637,21 @@ button:disabled {
581
  opacity: 0.82;
582
  }
583
 
584
- .research-event-title {
585
- margin-bottom: 6px;
586
  color: #fbbf24;
587
  font-weight: 600;
588
- font-size: 0.9rem;
589
  }
590
 
591
- .research-event details.tool-log > summary {
 
 
 
 
592
  cursor: pointer;
593
- color: #e5e7eb;
594
- font-weight: 600;
 
595
  }
596
 
597
  .research-event .tool-log-section {
 
328
  color: #e5e7eb;
329
  }
330
 
331
+ .tool-log-compact > summary.tool-compact-summary {
332
+ color: #fbbf24;
333
+ font-weight: 600;
334
+ }
335
+
336
+ .tool-compact-detail {
337
+ margin-top: 4px;
338
+ font-size: 0.82rem;
339
+ line-height: 1.4;
340
+ color: #9ca3af;
341
+ white-space: pre-wrap;
342
+ }
343
+
344
+ details.tool-log-raw {
345
+ margin-top: 8px;
346
+ }
347
+
348
+ details.tool-log-raw > summary {
349
+ cursor: pointer;
350
+ font-size: 0.75rem;
351
+ color: #6b7280;
352
+ }
353
+
354
+ .plan-message > summary {
355
+ color: #fbbf24;
356
+ font-weight: 700;
357
+ }
358
+
359
+ .plan-todo-list {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 8px;
363
+ margin-top: 8px;
364
+ }
365
+
366
+ .plan-todo-item {
367
+ padding: 8px 10px;
368
+ border-radius: 8px;
369
+ border: 1px solid rgba(148, 163, 184, 0.16);
370
+ background: rgba(15, 23, 42, 0.55);
371
+ }
372
+
373
+ .plan-todo-country {
374
+ font-weight: 700;
375
+ color: #f9fafb;
376
+ font-size: 0.9rem;
377
+ line-height: 1.3;
378
+ }
379
+
380
+ .plan-todo-methods {
381
+ margin-top: 3px;
382
+ font-size: 0.8rem;
383
+ line-height: 1.35;
384
+ color: #9ca3af;
385
+ }
386
+
387
  .markdown-body > :first-child {
388
  margin-top: 0;
389
  }
 
637
  opacity: 0.82;
638
  }
639
 
640
+ .research-event details.tool-log-compact > summary {
641
+ cursor: default;
642
  color: #fbbf24;
643
  font-weight: 600;
 
644
  }
645
 
646
+ .research-event .tool-compact-detail {
647
+ color: #cbd5e1;
648
+ }
649
+
650
+ .research-event details.tool-log-raw > summary {
651
  cursor: pointer;
652
+ color: #6b7280;
653
+ font-weight: 500;
654
+ font-size: 0.75rem;
655
  }
656
 
657
  .research-event .tool-log-section {
ui/agent/graph/respond.py CHANGED
@@ -21,11 +21,12 @@ from ..synthesis import build_structured_final_answer
21
  from .workflow import build_workflow
22
 
23
 
24
- def _format_plan_content(todos: list[dict[str, Any]]) -> str:
25
- lines = ["Research to-dos:"]
26
- for todo in todos:
27
- lines.append(f"{todo['id']}. {todo['country']} {todo['methods']}")
28
- return "\n".join(lines)
 
29
 
30
 
31
  def _research_task_country_methods(
@@ -93,6 +94,7 @@ class _UiState:
93
  metadata={
94
  "title": "Destination discovery",
95
  "status": "done",
 
96
  "log": event.get("log") or {},
97
  },
98
  )
@@ -104,11 +106,12 @@ class _UiState:
104
  self.ui_messages.append(
105
  ChatMessage(
106
  role="assistant",
107
- content=_format_plan_content(todos),
108
  metadata={
109
  "title": "Research plan",
110
  "status": "done",
111
- "log": {"tool": "plan_research", "result": todos},
 
112
  },
113
  )
114
  )
@@ -148,6 +151,7 @@ class _UiState:
148
  return True
149
 
150
  if kind == "tool_start":
 
151
  self.ui_messages.append(
152
  ChatMessage(
153
  role="assistant",
@@ -156,7 +160,8 @@ class _UiState:
156
  "title": str(event.get("title") or "Tool"),
157
  "status": "pending",
158
  "log": event.get("log") or {},
159
- "research_task": event.get("research_task"),
 
160
  },
161
  )
162
  )
@@ -166,6 +171,7 @@ class _UiState:
166
 
167
  if kind == "tool_end":
168
  index = self._pending_by_id.pop(str(event.get("id")), None)
 
169
  message = ChatMessage(
170
  role="assistant",
171
  content=str(event.get("message") or ""),
@@ -174,7 +180,8 @@ class _UiState:
174
  "status": "done",
175
  "duration": event.get("duration"),
176
  "log": event.get("log") or {},
177
- "research_task": event.get("research_task"),
 
178
  },
179
  )
180
  if index is not None and index < len(self.ui_messages):
 
21
  from .workflow import build_workflow
22
 
23
 
24
+ def _format_plan_summary(todos: list[dict[str, Any]]) -> str:
25
+ count = len(todos)
26
+ if count == 0:
27
+ return "No countries queued for research."
28
+ noun = "country" if count == 1 else "countries"
29
+ return f"{count} {noun} queued for parallel research."
30
 
31
 
32
  def _research_task_country_methods(
 
94
  metadata={
95
  "title": "Destination discovery",
96
  "status": "done",
97
+ "compact": True,
98
  "log": event.get("log") or {},
99
  },
100
  )
 
106
  self.ui_messages.append(
107
  ChatMessage(
108
  role="assistant",
109
+ content=_format_plan_summary(todos),
110
  metadata={
111
  "title": "Research plan",
112
  "status": "done",
113
+ "display": "plan",
114
+ "plan_todos": todos,
115
  },
116
  )
117
  )
 
151
  return True
152
 
153
  if kind == "tool_start":
154
+ research_task = event.get("research_task")
155
  self.ui_messages.append(
156
  ChatMessage(
157
  role="assistant",
 
160
  "title": str(event.get("title") or "Tool"),
161
  "status": "pending",
162
  "log": event.get("log") or {},
163
+ "research_task": research_task,
164
+ "compact": bool(research_task),
165
  },
166
  )
167
  )
 
171
 
172
  if kind == "tool_end":
173
  index = self._pending_by_id.pop(str(event.get("id")), None)
174
+ research_task = event.get("research_task")
175
  message = ChatMessage(
176
  role="assistant",
177
  content=str(event.get("message") or ""),
 
180
  "status": "done",
181
  "duration": event.get("duration"),
182
  "log": event.get("log") or {},
183
+ "research_task": research_task,
184
+ "compact": bool(research_task),
185
  },
186
  )
187
  if index is not None and index < len(self.ui_messages):
ui/agent/tools.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
  import json
5
  import time
6
  from typing import Any
 
7
 
8
  from gradio import ChatMessage
9
 
@@ -122,21 +123,54 @@ def _join(items: list[str], fallback: str) -> str:
122
  return ", ".join(clean) if clean else fallback
123
 
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  def _pending_tool_message(tool_name: str, args: dict[str, Any]) -> tuple[str, str]:
 
126
  if tool_name == "get_country_profile":
127
  countries = _join(args.get("countries") or [], "selected countries")
128
- return "Country profiles", f"Looking up country metadata for {countries}."
129
  if tool_name == "search_immigration_info":
130
  query = truncate(str(args.get("query") or "immigration sources"), 180)
131
- return "Searching sources", f"Searching for official immigration information: {query}"
132
  if tool_name == "scrape_web_page":
133
- return "Reading official page", f"Reading {args.get('url', 'the selected page')}."
134
  if tool_name == "crawl_web_site":
135
- return "Crawling source site", f"Crawling related pages from {args.get('url', 'the selected site')}."
136
  if tool_name == "update_globe":
137
  countries = _join(args.get("countries") or [], "the selected countries")
138
- return "Updating globe", f"Showing {countries} on the globe."
139
- return f"Using {tool_name}", f"Running `{tool_name}`."
140
 
141
 
142
  def _format_log_result(result: str) -> Any:
 
4
  import json
5
  import time
6
  from typing import Any
7
+ from urllib.parse import urlparse
8
 
9
  from gradio import ChatMessage
10
 
 
123
  return ", ".join(clean) if clean else fallback
124
 
125
 
126
+ def _url_host_or_path(url: str, *, limit: int = 80) -> str:
127
+ raw = str(url or "").strip()
128
+ if not raw:
129
+ return "page"
130
+ try:
131
+ parsed = urlparse(raw if "://" in raw else f"https://{raw}")
132
+ host = parsed.netloc
133
+ path = parsed.path.strip("/")
134
+ if host and path:
135
+ return truncate(f"{host}/{path}", limit)
136
+ return truncate(host or path or raw, limit)
137
+ except Exception:
138
+ return truncate(raw, limit)
139
+
140
+
141
+ def tool_display_title(tool_name: str, args: dict[str, Any]) -> str:
142
+ if tool_name == "get_country_profile":
143
+ countries = _join(args.get("countries") or [], "selected countries")
144
+ return f"Profiles · {countries}"
145
+ if tool_name == "search_immigration_info":
146
+ query = truncate(str(args.get("query") or "immigration sources"), 80)
147
+ return f"Search · {query}"
148
+ if tool_name == "scrape_web_page":
149
+ return f"Read · {_url_host_or_path(str(args.get("url") or ""))}"
150
+ if tool_name == "crawl_web_site":
151
+ return f"Crawl · {_url_host_or_path(str(args.get("url") or ""), limit=60)}"
152
+ if tool_name == "update_globe":
153
+ countries = _join(args.get("countries") or [], "selected countries")
154
+ return f"Globe · {countries}"
155
+ return f"Using {tool_name}"
156
+
157
+
158
  def _pending_tool_message(tool_name: str, args: dict[str, Any]) -> tuple[str, str]:
159
+ title = tool_display_title(tool_name, args)
160
  if tool_name == "get_country_profile":
161
  countries = _join(args.get("countries") or [], "selected countries")
162
+ return title, f"Looking up country metadata for {countries}."
163
  if tool_name == "search_immigration_info":
164
  query = truncate(str(args.get("query") or "immigration sources"), 180)
165
+ return title, f"Searching for official immigration information: {query}"
166
  if tool_name == "scrape_web_page":
167
+ return title, f"Reading {args.get('url', 'the selected page')}."
168
  if tool_name == "crawl_web_site":
169
+ return title, f"Crawling related pages from {args.get('url', 'the selected site')}."
170
  if tool_name == "update_globe":
171
  countries = _join(args.get("countries") or [], "the selected countries")
172
+ return title, f"Showing {countries} on the globe."
173
+ return title, f"Running `{tool_name}`."
174
 
175
 
176
  def _format_log_result(result: str) -> Any: