Skip to content
27 changes: 17 additions & 10 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { defineConfig, devices } from '@playwright/test'

const baseURL = process.env.BASE_URL || 'http://localhost:3000'
const baseURL = process.env.BASE_URL || 'http://localhost:3001'
const isRemote = baseURL.startsWith('https://')

export default defineConfig({
testDir: './tests',
globalTeardown: './tests/global-teardown.ts',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
retries: process.env.CI ? 2 : 1,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
reporter: [['html'], ['list']],
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'off',
},

projects: [
Expand All @@ -23,19 +24,25 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
...(process.env.CI ? [
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
] : []),
],

...(isRemote ? {} : {
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
url: 'http://localhost:3001',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
Expand Down
452 changes: 153 additions & 299 deletions frontend/src/app/dashboard/page.tsx

Large diffs are not rendered by default.

30 changes: 25 additions & 5 deletions frontend/src/app/documents/[id]/graph/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,36 @@ export default function GraphPage() {

const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const elapsedIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const extractionStartRef = useRef<number | null>(null)
const [extractionElapsed, setExtractionElapsed] = useState(0)

// Cleanup polling on unmount
useEffect(() => {
return () => {
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current)
if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current)
if (elapsedIntervalRef.current) clearInterval(elapsedIntervalRef.current)
}
}, [])

// Track elapsed time during extraction
useEffect(() => {
if (extracting) {
extractionStartRef.current = Date.now()
setExtractionElapsed(0)
elapsedIntervalRef.current = setInterval(() => {
setExtractionElapsed(Math.floor((Date.now() - (extractionStartRef.current ?? Date.now())) / 1000))
}, 1000)
} else {
if (elapsedIntervalRef.current) clearInterval(elapsedIntervalRef.current)
extractionStartRef.current = null
}
return () => {
if (elapsedIntervalRef.current) clearInterval(elapsedIntervalRef.current)
}
}, [extracting])

const triggerExtraction = async () => {
setExtracting(true)
try {
Expand Down Expand Up @@ -813,10 +834,9 @@ export default function GraphPage() {
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<Network className="w-16 h-16 text-ink-700 mx-auto" />
<h3 className="font-display text-lg font-semibold mt-4">No Entities Extracted</h3>
<h3 className="font-display text-lg font-semibold mt-4">No knowledge graph yet</h3>
<p className="text-ink-500 mt-2 max-w-md">
Extract entities and relationships from this document to visualize
the knowledge graph.
Build the graph for this contract - extract parties, dates, amounts, and the relationships between them.
</p>
<button
onClick={triggerExtraction}
Expand All @@ -826,12 +846,12 @@ export default function GraphPage() {
{extracting ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-2" />
Extracting...
Analyzing{extractionElapsed > 0 ? ` · ${extractionElapsed}s` : '...'}
</>
) : (
<>
<Network className="w-4 h-4 mr-2" />
Extract Entities
Build Knowledge Graph
</>
)}
</button>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/documents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ export default function DocumentDetailPage() {
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-ink-100">
{clauseTypeLabels[clause.clause_type] || clause.clause_type.replace(/_/g, ' ')}
{clauseTypeLabels[clause.clause_type] || (clause.clause_type ?? '').replace(/_/g, ' ')}
</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium uppercase ${risk.color} ${risk.bg}/10`}>
{clause.risk_level}
Expand Down Expand Up @@ -930,7 +930,7 @@ export default function DocumentDetailPage() {
{clause.risk_level}
</span>
<div>
<span className="text-sm font-medium text-ink-200">{clause.clause_type}</span>
<span className="text-sm font-medium text-ink-200">{clauseTypeLabels[clause.clause_type] || (clause.clause_type ?? '').replace(/_/g, ' ')}</span>
<p className="text-xs text-ink-400 mt-0.5 line-clamp-2">{clause.summary}</p>
</div>
</div>
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/lib/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { usePathname } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
import { LayoutDashboard, BarChart3, GitCompareArrows, Search, Menu, X, Sun, Moon, ClipboardCheck, Briefcase } from 'lucide-react'
import { LayoutDashboard, BarChart3, GitCompareArrows, Menu, X, Sun, Moon, ClipboardCheck, Briefcase } from 'lucide-react'
import { useState } from 'react'
import { useTheme } from '@/lib/theme'

Expand All @@ -13,7 +13,6 @@ const navLinks = [
{ href: '/compare', label: 'Compare', icon: GitCompareArrows },
{ href: '/obligations', label: 'Obligations', icon: ClipboardCheck },
{ href: '/deals', label: 'Deals', icon: Briefcase },
{ href: '/search', label: 'Search', icon: Search },
]

export function Navigation({ children }: { children?: React.ReactNode }) {
Expand All @@ -36,7 +35,7 @@ export function Navigation({ children }: { children?: React.ReactNode }) {
/>
<div className="hidden sm:block">
<span className="font-display text-xl font-bold tracking-tight text-ink-50">BrightClause</span>
<p className="text-[10px] text-ink-500 tracking-wide uppercase font-mono">Contract Intelligence</p>
<p className="text-[10px] text-ink-500 tracking-wide">Contract Intelligence</p>
</div>
</Link>

Expand Down
24 changes: 12 additions & 12 deletions frontend/src/lib/walkthrough.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,32 @@ interface WalkthroughStep {
const WALKTHROUGH_STEPS: WalkthroughStep[] = [
{
target: '[data-tour="stats"]',
title: 'Portfolio Overview',
description: 'Real-time statistics showing your indexed documents, text chunks with vector embeddings, extracted clauses, and documents ready for review.',
title: 'Your contract portfolio',
description: 'A live snapshot of every contract in your portfolio - how many you have, how many clauses have been found, and which ones are ready to review.',
position: 'bottom',
},
{
target: '[data-tour="search"]',
title: 'Semantic Search',
description: 'Search across all contracts using natural language. Our hybrid search combines AI semantic understanding with keyword matching for precise results.',
title: 'Search across all contracts',
description: 'Ask a question in plain English and find the answer across every contract at once. Try: "does any contract limit our liability to under $50k?"',
position: 'bottom',
},
{
target: '[data-tour="upload"]',
title: 'Upload Contracts',
description: 'Upload PDF contracts for automated processing. Documents are parsed, chunked, embedded, and analyzed by our AI pipeline.',
title: 'Add a contract',
description: 'Drop any PDF contract here. Within a few minutes, every clause is read, categorized by type, and given a risk rating - automatically.',
position: 'bottom',
},
{
target: '[data-tour="documents"]',
title: 'Contract Portfolio',
description: 'Click any document to view its AI risk assessment. Each contract is analyzed for clause types, risk levels, and key provisions.',
title: 'Click to see risk',
description: 'Click any contract to see its overall risk rating. Contracts with critical or high-risk clauses are flagged so you know where to focus first.',
position: 'right',
},
{
target: '[data-tour="analysis"]',
title: 'Risk Assessment Panel',
description: 'AI-powered analysis shows overall risk, risk distribution, and highlights clauses that need attention. Click "View Full Analysis" for detailed clause-by-clause review.',
title: 'Risk breakdown',
description: 'The risk panel shows the overall exposure and which specific clauses need attention. Click "View Full Analysis" to read each flagged clause with a plain-English explanation.',
position: 'left',
},
]
Expand Down Expand Up @@ -179,8 +179,8 @@ export function WalkthroughOverlay({
<div className="px-5 py-4 border-b border-ink-800/50 flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-accent" />
<span className="text-xs font-mono text-ink-300 uppercase tracking-wide">
Step {step + 1} of {totalSteps}
<span className="text-xs text-ink-400">
{step + 1} of {totalSteps}
</span>
</div>
<button
Expand Down
Loading