Commit 493ee9c
Deep research: frontend (report view, corpus tab, notifications) + chat status tool (#1836)
* Add deep-research chat status tool + slug query resolver
Backend groundwork for the deep-research frontend (the v1 feature shipped
backend-only; the completion chat message links to /research/{slug} but
nothing resolved that route, and the chat agent could start a job but not
report on one).
- research_queries.py: add researchReportBySlug(slug) resolver (creator-only
via BaseService.filter_visible; IDOR-safe null for non-owners/unknown slugs)
so the frontend can resolve the /research/{slug} route.
- research_tools.py: add check_deep_research_status chat tool (read-only) so a
follow-up like "is my research done?" returns status/progress + the report
link instead of being a dead end.
- research_reports.py: add ResearchReportService.list_recent_for_corpus read
helper (creator-only, bounded) backing the tool.
- pydantic_ai_agents.py: register the status tool alongside the kickoff tool on
the corpus chat agent; both are filtered out of the read-only research agent
via restrict_tool_names.
- tests: slug-resolver GraphQL tests + status-tool/service tests.
* Build deep-research frontend: report view, corpus tab, notifications
Completes the backend-only v1 (#1814) so users can monitor and read
deep-research reports. The completion chat message linked to /research/{slug}
but no route resolved it; RESEARCH_REPORT_* notifications weren't surfaced;
and there was no report view or list.
Frontend:
- GraphQL + cache: research queries/mutations, JobStatus/ResearchReportType
types, openedResearchReport reactive var, relayStylePagination keyed by the
GraphQL field-arg names (corpusId/status) + researchReportBySlug keyArgs.
- Routing: standalone /research/:slug resolved by CentralRouteManager Phase 1
(slug-based) with a Phase-3 canonical-redirect guard; openedResearchReport
added to the routing write-discipline allowlist.
- Views: ResearchReportDetail (SafeMarkdown body with footnote citations;
Report/Citations/Sources/Run-details tabs; live polling + Cancel while
running; citations deep-link to the cited annotation) and a corpus
'Research' tab listing reports. Secondary StartResearchModal for an explicit
kickoff (primary trigger stays the chat agent).
- Notifications: RESEARCH_REPORT_* wired into the notification type union +
job-toast system (clickable, deep-links to /research/:slug), terminal cache
updates, and a completion hook the detail view uses to refetch + stop
polling.
Tests: frontend unit tests (route parsing + research utils) and a Playwright
component test for the detail view. Typescript compiles; unit + routing
discipline tests pass.
* Apply black formatting to research status-tool test
* Remove unused loading binding from research detail useQuery
CodeQL flagged the destructured `loading` from the GET_RESEARCH_REPORT
useQuery as unused (the running/cancel states use the mutation's
`cancelLoading`, not the query's). No behavior change.
* Fix research detail CT crash: inline corpus back-URL
The Playwright CT (esbuild dev) bundler failed to bind the named getCorpusUrl
import in ResearchReportDetail, throwing 'getCorpusUrl is not defined' at
render so the report never mounted (all 3 component tests failed). getDocumentUrl
from the same module binds fine. Inline the corpus back-navigation URL (same
/c/{creator}/{corpus} shape) and drop the getCorpusUrl import. The 3
ResearchReportDetail component tests now pass locally.
* Fix research detail CT: mock had both variableMatcher and request.variables
The real cause of the Component Tests failure: MockedProvider rejects a mock
that supplies both request.variables and variableMatcher ("should contain
either variableMatcher or request.variables"), so the provider constructor
threw and the component never mounted. Keep the explicit request.variables
({ id }) form and drop the redundant variableMatcher.
* Address review: client-side citation links, effect-guarded error toast, hook cleanups
- ResearchReportDetail: RowLink is now styled(react-router Link) with to= so
internal document/citation links route client-side instead of full-reload.
- CorpusResearchReportCards: move the error toast into a useEffect so it fires
once per error rather than on every re-render.
- useResearchCompletionNotification: memoise numericId (feeds useCallback deps)
and drop a leftover console.debug.
- Add ResearchReportListCard component tests (completed + running) with a
documentation screenshot.
* Polish review items: color tokens, modal reset, Queued filter, cancel feedback
Follow-up to the review fixes already on this branch:
- ResearchReportDetail: replace remaining hardcoded hex (#ffffff/#fff7ed/
#9a3412) with OS_LEGAL_COLORS tokens (surface/warningSurface/warningText);
stable keys (key={c.footnote} for citations, key=index+value for warnings);
Cancel button shows 'Cancelling…' and disables while cancelRequested is
pending.
- StartResearchModal: reset prompt/title on close so a reopened modal starts
blank.
- ResearchTabContent: add the 'Queued' filter tab (backend status map already
supports it; a fresh job sits in QUEUED briefly).
Typescript compiles; research component tests pass.
* Address review: clear openedResearchReport in all route handlers, restore getCorpusUrl helper, tokenize toast colors, reset research search on unmount
- Clear openedResearchReport(null) in thread/labelset/user route handlers (sibling of openedExtract, was omitted)
- Replace inlined corpus URL in ResearchReportDetail.handleBack with getCorpusUrl(corpus, {tab}); safer navigate('/') fallback
- JobNotificationToast: source status colors from OS_LEGAL_COLORS tokens instead of raw hex
- ResearchTabContent: reset researchSearchTerm and use DEBOUNCE.LIST_SEARCH_MS; clarify empty-state copy + add Load More in CorpusResearchReportCards
- ResearchReportListCard: console.warn on missing slug instead of silent no-op
* Add CorpusResearchReportCards smoke test + research report detail screenshot
- New CorpusResearchReportCards.ct.tsx + wrapper: mounts the corpus Research tab list, asserts query wiring + empty state, captures a screenshot (review item 10). Populated-list assertion omitted — the component's network-only query re-fires across renders and MockedProvider can't serve it deterministically in isolation; card visuals are covered by ResearchReportListCard.ct.tsx.
- ResearchReportDetail.ct.tsx: add docScreenshot for the completed-report state.
* Address review: fix citation annotation typename, status type, toolbar token
- Fix citation deep-link annotation typename mismatch in ResearchReportDetail:
build the annotation global ID from fullSourceAnnotationList (the canonical
ServerAnnotationType id the backend emits) via an annGlobalIdByPk map,
instead of reconstructing toGlobalId('AnnotationType', pk) which guessed the
wrong type name and produced deep-links resolving to the wrong entity. Drop
the now-unused toGlobalId import.
- Add a CT regression guard asserting the citation link's ?ann= param carries
the ServerAnnotationType global id.
- Type GetResearchReportsInput.status as JobStatus | undefined (was loose
string) and tighten FILTER_TO_STATUS to match.
- Use OS_LEGAL_COLORS.surface for the research Toolbar background instead of a
hardcoded 'white' literal, consistent with the other toolbars in this PR.
* Address review: seed ResearchReportDetail test reactive var synchronously
ResearchReportDetailTestWrapper seeded openedResearchReport in a useEffect,
which fires only after the child's first render — so ResearchReportDetail
flashed its 'not found' state for one frame before the report appeared, a
flakiness risk on slow CI. Switch to the synchronous useState-initializer
pattern (matching CorpusResearchReportCardsTestWrapper) so the var is seeded
before the first render, keeping a useEffect solely for unmount cleanup.
* Address review: research-report routing, citation keys, double-fetch guard
- CentralRouteManager: call routeLoading(false) before navigate('/404')
in both the error and null-data branches of the research-report block.
- ResearchReportRoute: remove dead error branch (CentralRouteManager
redirects to /404 on error/null, never sets routeError for research).
- ResearchReportDetail: key citation rows by array index, not c.footnote,
to avoid React key collisions from duplicate footnotes in JSON.
- StartResearchModal: navigate via getResearchReportUrl(payload.obj).
- ResearchTabContent: widen setActiveTab prop to (number | string).
- CorpusResearchReportCards: mount-guard the refetch effect to avoid a
double fetch on initial mount; add explicit 'Could not load reports'
error render when error && no reports loaded.
- navigationUtils: drop production console.warn in getResearchReportUrl
('#' sentinel already signals the caller).
- useNotificationWebSocket: TODO(v2) on RESEARCH_REPORT_PROGRESS union member.
* Fix research report card tests + harden fetch/error states
- CorpusResearchReportCardsTestWrapper: set addTypename:false on the
custom InMemoryCache so it agrees with MockedProvider; a typename-adding
cache injected __typename into the query the mock link matched against,
draining the mock bucket and resolving the final fire to an error.
- CorpusResearchReportCards: switch fetchPolicy to cache-and-network
(nextFetchPolicy cache-first) so re-renders serve from cache instead of
a network-only refetch storm; only surface the error state once the
request has settled (error && !loading) so a transient retry error
doesn't flash over the spinner.
- ResearchReportDetail.ct: add research--report-detail--citations
docScreenshot after the citations-tab assertions.
* Address review: research status/util hardening + StartResearchModal test
- Remove dead RESEARCH_REPORT_PROGRESS member from the notification type union
(no v1 emitter/handler); add it when the handler is wired.
- getResearchStatus dev-warns on an unrecognized JobStatus instead of silently
rendering 'Queued' (null/undefined still treated as the not-set-yet case).
- Align the chat status tool's duration string with the frontend
formatResearchDuration helper (sub-minute reads '42s', not '0m 42s'); add a
regression test.
- Cap StartResearchModal's optional title input at MAX_RESEARCH_TITLE_CHARS
(255, mirroring the ResearchReport.title model column) + new smoke CT.
- Key citation rows off the unique footnote, not the array index.
- Add anonymous-user rejection test pinning @login_required on
researchReportBySlug.
- Clarify the cache-and-network/cache-first nextFetchPolicy comment.
* Address review: atomic research finalize + clear report state on route change
- finalize() now wraps the terminal content/status save and the
source_annotations/source_documents M2M set() in a single
transaction.atomic() block, so an interrupted Celery task can no longer
leave a COMPLETED report whose citations have no backing provenance rows.
- CentralRouteManager clears openedResearchReport in Phase 1 whenever the
resolved route is no longer a research route (single source of truth),
closing the stale-state gap on /research/:slug -> corpus/document routes
and browser back/forward. Regression-guarded in centralRouteDiscipline.test.ts.
* Apply black formatting to research_reports.py
Collapse a source_documents.set(Document.objects.filter(...)) call onto one
line — black flagged it (the only pre-commit/linter violation on HEAD;
isort/flake8/pyupgrade all clean). Formatting only, no behavior change.
* Address PR #1836 review: permission constant, terminal-status polling guard, CHANGELOG indent
- Extract the research-report update permission string into a
RESEARCH_REPORT_UPDATE_PERMISSION constant so a typo can't silently
disable Cancel for all users (review #2).
- isTerminalResearchStatus now treats an unrecognized status as terminal
so the detail view never polls indefinitely on a future backend state
the frontend doesn't recognize; null/Queued/Running stay non-terminal
(review #3). Added a unit-test case.
- Fix a lost 2-space continuation indent in the CHANGELOG SmartLabel
entry (review #5).
* fix(research): address review on deep-research frontend (#1814)
- graphql-api: add the backend's transient JobStatus.CREATED to the frontend
enum; it was missing, so isTerminalResearchStatus(CREATED) wrongly returned
true and stopped status polling before the job left the pre-queue state
- researchUtils: treat CREATED as non-terminal and display it as 'Queued'
- ResearchReportDetail: rename isRunning -> isActive (it covers created/queued/
running), key warning chips by their text instead of array index, log the
swallowed error in the cancel catch block
- StartResearchModal: log the swallowed error in the submit catch block
- tests: cover CREATED in getResearchStatus and isTerminalResearchStatus
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 806d41b commit 493ee9c
42 files changed
Lines changed: 3611 additions & 45 deletions
File tree
- config/graphql
- docs/assets/images/screenshots/auto
- frontend
- src
- assets/configurations
- components
- notifications
- research
- routes
- widgets/modals
- graphql
- hooks
- routing
- __tests__
- types
- utils
- __tests__
- views
- tests
- opencontractserver
- llms
- agents
- tools
- research/services
- tests/research
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
| |||
406 | 407 | | |
407 | 408 | | |
408 | 409 | | |
| 410 | + | |
409 | 411 | | |
410 | 412 | | |
411 | 413 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| 131 | + | |
| 132 | + | |
131 | 133 | | |
132 | 134 | | |
133 | 135 | | |
| |||
229 | 231 | | |
230 | 232 | | |
231 | 233 | | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
232 | 279 | | |
233 | 280 | | |
234 | 281 | | |
| |||
Lines changed: 43 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
9 | 11 | | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| 15 | + | |
13 | 16 | | |
14 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
15 | 26 | | |
16 | 27 | | |
17 | 28 | | |
| 29 | + | |
18 | 30 | | |
19 | 31 | | |
20 | 32 | | |
| |||
62 | 74 | | |
63 | 75 | | |
64 | 76 | | |
65 | | - | |
| 77 | + | |
66 | 78 | | |
67 | 79 | | |
68 | 80 | | |
69 | 81 | | |
70 | 82 | | |
71 | 83 | | |
72 | | - | |
| 84 | + | |
73 | 85 | | |
74 | 86 | | |
75 | 87 | | |
| |||
78 | 90 | | |
79 | 91 | | |
80 | 92 | | |
81 | | - | |
| 93 | + | |
82 | 94 | | |
83 | 95 | | |
84 | 96 | | |
85 | 97 | | |
86 | 98 | | |
87 | 99 | | |
88 | | - | |
| 100 | + | |
89 | 101 | | |
90 | 102 | | |
91 | 103 | | |
92 | 104 | | |
93 | 105 | | |
94 | 106 | | |
95 | | - | |
| 107 | + | |
96 | 108 | | |
97 | 109 | | |
98 | 110 | | |
99 | 111 | | |
100 | 112 | | |
101 | 113 | | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
102 | 135 | | |
103 | 136 | | |
104 | 137 | | |
105 | 138 | | |
| 139 | + | |
| 140 | + | |
106 | 141 | | |
107 | 142 | | |
108 | 143 | | |
| |||
111 | 146 | | |
112 | 147 | | |
113 | 148 | | |
| 149 | + | |
114 | 150 | | |
115 | 151 | | |
116 | 152 | | |
| |||
119 | 155 | | |
120 | 156 | | |
121 | 157 | | |
122 | | - | |
| 158 | + | |
123 | 159 | | |
124 | 160 | | |
125 | 161 | | |
| |||
0 commit comments