Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cf270eb
Add deep-research chat status tool + slug query resolver
claude May 30, 2026
de2323c
Build deep-research frontend: report view, corpus tab, notifications
claude May 30, 2026
58b061b
Apply black formatting to research status-tool test
claude May 30, 2026
09f5fdf
Remove unused loading binding from research detail useQuery
claude May 30, 2026
5ef0903
Fix research detail CT crash: inline corpus back-URL
claude May 30, 2026
752b39e
Fix research detail CT: mock had both variableMatcher and request.var…
claude May 30, 2026
c20794c
Merge remote-tracking branch 'origin/main' into claude/inspiring-knut…
JSv4 May 30, 2026
41370eb
Address review: client-side citation links, effect-guarded error toas…
JSv4 May 30, 2026
57ca66f
Polish review items: color tokens, modal reset, Queued filter, cancel…
claude May 30, 2026
f17958d
Merge remote-tracking branch 'origin/main' into claude/inspiring-knut…
JSv4 May 30, 2026
6631898
Address review: clear openedResearchReport in all route handlers, res…
JSv4 May 30, 2026
053fdac
Add CorpusResearchReportCards smoke test + research report detail scr…
JSv4 May 30, 2026
c5eef18
Address review: fix citation annotation typename, status type, toolba…
JSv4 May 30, 2026
244c96d
Merge remote-tracking branch 'origin/main' into claude/inspiring-knut…
JSv4 May 30, 2026
9892fa8
Address review: seed ResearchReportDetail test reactive var synchrono…
JSv4 May 30, 2026
a3dfc68
Merge origin/main into deep-research frontend branch
JSv4 May 30, 2026
c842dc9
Address review: research-report routing, citation keys, double-fetch …
JSv4 May 30, 2026
76c3ce8
Fix research report card tests + harden fetch/error states
JSv4 May 30, 2026
90d0f4a
Merge remote-tracking branch 'origin/main' into claude/inspiring-knut…
JSv4 May 31, 2026
e2d87b4
Address review: research status/util hardening + StartResearchModal test
JSv4 May 31, 2026
9ca46dd
Address review: atomic research finalize + clear report state on rout…
JSv4 May 31, 2026
61bcdda
Apply black formatting to research_reports.py
claude May 31, 2026
48a3902
Merge remote-tracking branch 'origin/main' into claude/inspiring-knut…
JSv4 May 31, 2026
394ef73
Address PR #1836 review: permission constant, terminal-status polling…
JSv4 May 31, 2026
be84c74
Merge origin/main into claude/inspiring-knuth-bhtzs
JSv4 May 31, 2026
f1b24bd
fix(research): address review on deep-research frontend (#1814)
JSv4 May 31, 2026
00872ce
Merge remote-tracking branch 'origin/main' into claude/inspiring-knut…
JSv4 May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions CHANGELOG.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions config/graphql/research_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,25 @@ def resolve_research_reports(self, info, **kwargs) -> Any:
return qs.none()
qs = qs.filter(status=status)
return qs.order_by("-created")

research_report_by_slug = graphene.Field(
ResearchReportType,
slug=graphene.String(required=True),
description=(
"Fetch a single research report by its unique slug. The "
"deep-research completion chat message links to /research/{slug}, "
"so the frontend resolves that route through this field. "
"Creator-only visibility (returns null for non-owners or unknown "
"slugs — IDOR-safe)."
),
)

@login_required
def resolve_research_report_by_slug(self, info, slug) -> Any:
return (
BaseService.filter_visible(
ResearchReport, info.context.user, request=info.context
)
.filter(slug=slug)
.first()
)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
} from "./components/admin";
import { useEnv } from "./components/hooks/UseEnv";
import { ExtractDetailRoute } from "./components/routes/ExtractDetailRoute";
import { ResearchReportRoute } from "./components/routes/ResearchReportRoute";
import { FileUploadPackageProps } from "./components/widgets/modals/DocumentUploadModal";
import { DocumentLandingRoute } from "./components/routes/DocumentLandingRoute";
import { LabelSetLandingRoute } from "./components/routes/LabelSetLandingRoute";
Expand Down Expand Up @@ -406,6 +407,7 @@ export const App = () => {
<Route path="/about" element={<About />} />
<Route path="/extracts/:extractId" element={<ExtractDetailRoute />} />
<Route path="/extracts" element={<Extracts />} />
<Route path="/research/:slug" element={<ResearchReportRoute />} />
<Route path="/admin/badges" element={<BadgeManagement />} />
<Route path="/admin/settings" element={<GlobalSettingsPanel />} />
<Route path="/admin/agents" element={<GlobalAgentManagement />} />
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/assets/configurations/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export const SELECTION_MENU = {
export const DEBOUNCE = {
SEARCH_MS: 1000,
EXTRACT_SEARCH_MS: 500,
/** Generic debounce for client-side list search boxes (extracts, research, …). */
LIST_SEARCH_MS: 500,
CLICK_OUTSIDE_DELAY_MS: 100,
CORPUS_SEARCH_MS: 400,
CORPUS_SEARCH_MAX_WAIT_MS: 1000,
Expand Down Expand Up @@ -229,6 +231,51 @@ export const EXTRACT_STATUS_COLORS = {
[EXTRACT_STATUS.NOT_STARTED]: "default",
} as const;

// Deep-research report status constants.
// Keys match the backend JobStatus enum values
// (opencontractserver/types/enums.py); values are the display labels.
export const RESEARCH_STATUS = {
QUEUED: "Queued",
RUNNING: "Researching",
COMPLETED: "Completed",
FAILED: "Failed",
CANCELLED: "Cancelled",
} as const;

export type ResearchStatusLabel =
(typeof RESEARCH_STATUS)[keyof typeof RESEARCH_STATUS];

// Research chip color mapping (Chip `color` tokens from @os-legal/ui)
export const RESEARCH_STATUS_COLORS = {
[RESEARCH_STATUS.QUEUED]: "default",
[RESEARCH_STATUS.RUNNING]: "info",
[RESEARCH_STATUS.COMPLETED]: "success",
[RESEARCH_STATUS.FAILED]: "error",
[RESEARCH_STATUS.CANCELLED]: "warning",
} as const;

// Poll interval (ms) the research report detail view uses while a job is
// non-terminal. The backend emits no per-step progress events in v1, so the
// view polls the (indexed, creator-only) single-report query for live
// stepCount/lastProgressAt and stops on the terminal WebSocket notification.
export const RESEARCH_REPORT_POLL_INTERVAL_MS = 5000;

// Guardian object-permission string that gates cancelling a research report.
// Centralised here (not inlined) so a typo can't silently disable Cancel for
// every user and any future check on the same model stays consistent.
export const RESEARCH_REPORT_UPDATE_PERMISSION = "update_researchreport";

// Max research prompt length. Mirrors the backend cap
// (opencontractserver/research/constants.py MAX_RESEARCH_PROMPT_CHARS) so the
// UI rejects over-long prompts before the round-trip.
export const MAX_RESEARCH_PROMPT_CHARS = 10000;

// Max research report title length. Mirrors the backend model column
// (opencontractserver/research/models.py ResearchReport.title max_length=255)
// so the UI caps the optional title before the round-trip rather than letting
// the DB silently truncate / the service reject it.
export const MAX_RESEARCH_TITLE_CHARS = 255;

// Tool usage UI constants (used by chat ToolUsageIndicator)
export const TOOL_UNKNOWN_LABEL = "Unknown Tool";

Expand Down
50 changes: 43 additions & 7 deletions frontend/src/components/notifications/JobNotificationToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@ import {
BarChart3,
Download,
XCircle,
Sparkles,
Ban,
LucideIcon,
} from "lucide-react";
import type { JobNotification } from "../../hooks/useJobNotifications";
import type { NotificationType } from "../../hooks/useNotificationWebSocket";
import { OS_LEGAL_COLORS } from "../../assets/configurations/osLegalStyles";

const ToastContainer = styled.div`
// Semantic status colors for job toasts, sourced from the design tokens so we
// don't scatter raw hex literals (no-magic-numbers). The IconContainer appends
// a "20" alpha suffix, so each must be a 6-digit hex.
const STATUS_SUCCESS = OS_LEGAL_COLORS.success;
const STATUS_FAILED = OS_LEGAL_COLORS.danger;
const STATUS_INFO = OS_LEGAL_COLORS.primaryBlue;
const STATUS_NEUTRAL = OS_LEGAL_COLORS.textMuted;

const ToastContainer = styled.div<{ $clickable?: boolean }>`
display: flex;
align-items: center;
gap: 12px;
cursor: ${({ $clickable }) => ($clickable ? "pointer" : "default")};
`;

const IconContainer = styled.div<{ $color: string }>`
Expand Down Expand Up @@ -62,14 +74,14 @@ interface JobNotificationConfig {
const JOB_NOTIFICATION_CONFIG: Record<string, JobNotificationConfig> = {
DOCUMENT_PROCESSED: {
icon: FileText,
color: "#4CAF50",
color: STATUS_SUCCESS,
title: "Document Ready",
getMessage: (data) =>
`"${(data.documentTitle as string) || "Document"}" finished processing`,
},
EXTRACT_COMPLETE: {
icon: Table2,
color: "#2196F3",
color: STATUS_INFO,
title: "Extract Complete",
getMessage: (data) =>
`"${(data.extractName as string) || "Extract"}" completed (${
Expand All @@ -78,31 +90,54 @@ const JOB_NOTIFICATION_CONFIG: Record<string, JobNotificationConfig> = {
},
ANALYSIS_COMPLETE: {
icon: BarChart3,
color: "#4CAF50",
color: STATUS_SUCCESS,
title: "Analysis Complete",
getMessage: (data) =>
`"${(data.analyzerName as string) || "Analysis"}" finished successfully`,
},
ANALYSIS_FAILED: {
icon: XCircle,
color: "#F44336",
color: STATUS_FAILED,
title: "Analysis Failed",
getMessage: (data) =>
`"${(data.analyzerName as string) || "Analysis"}" encountered an error`,
},
EXPORT_COMPLETE: {
icon: Download,
color: "#4CAF50",
color: STATUS_SUCCESS,
title: "Export Ready",
getMessage: (data) =>
`"${
(data.exportName as string) || (data.corpusName as string) || "Export"
}" is ready for download`,
},
RESEARCH_REPORT_COMPLETE: {
icon: Sparkles,
color: STATUS_SUCCESS,
title: "Research Complete",
getMessage: (data) =>
`"${(data.title as string) || "Research"}" is ready to read`,
},
RESEARCH_REPORT_FAILED: {
icon: XCircle,
color: STATUS_FAILED,
title: "Research Failed",
getMessage: (data) =>
`"${(data.title as string) || "Research"}" could not be completed`,
},
RESEARCH_REPORT_CANCELLED: {
icon: Ban,
color: STATUS_NEUTRAL,
title: "Research Cancelled",
getMessage: (data) =>
`"${(data.title as string) || "Research"}" was cancelled`,
},
};

export interface JobNotificationToastProps {
notification: JobNotification;
/** Optional click handler — e.g. deep-link a research toast to its report. */
onClick?: () => void;
}

/**
Expand All @@ -111,6 +146,7 @@ export interface JobNotificationToastProps {
*/
export function JobNotificationToast({
notification,
onClick,
}: JobNotificationToastProps) {
const config =
JOB_NOTIFICATION_CONFIG[notification.type] ||
Expand All @@ -119,7 +155,7 @@ export function JobNotificationToast({
const Icon = config.icon;

return (
<ToastContainer>
<ToastContainer $clickable={Boolean(onClick)} onClick={onClick}>
<IconContainer $color={config.color}>
<Icon />
</IconContainer>
Expand Down
Loading
Loading