Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions ui/src/components/planner/RevenueSources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ActionButton } from '../ui/ActionButton';
import { useToast } from '../../lib/toast';
import { Plus, X, Pencil, Check } from 'lucide-react';

const emptyForm = { description: '', amount: '', recurrence: 'monthly', source: 'manual', confidence: '0.8' };

export function RevenueSources() {
const [sources, setSources] = useState<RevenueSource[]>([]);
const [summary, setSummary] = useState({ count: 0, total_monthly: 0, weighted_monthly: 0 });
Expand All @@ -15,8 +17,6 @@ export function RevenueSources() {
const [editingId, setEditingId] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const toast = useToast();

const emptyForm = { description: '', amount: '', recurrence: 'monthly', source: 'manual', confidence: '0.8' };
const [form, setForm] = useState(emptyForm);

const load = useCallback(async () => {
Expand Down
36 changes: 17 additions & 19 deletions ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,21 @@ export interface TokenActionResponse {
result: unknown;
}

function buildQs(params?: Record<string, unknown>): string {
if (!params) return '';
const qs = new URLSearchParams(
Object.fromEntries(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])),
).toString();
return qs ? '?' + qs : '';
}

export const api = {
// Dashboard
getDashboard: () => request<DashboardData>('/dashboard'),

// Obligations
getObligations: (params?: { status?: string; category?: string }) => {
const qs = new URLSearchParams(params as Record<string, string>).toString();
return request<Obligation[]>(`/obligations${qs ? '?' + qs : ''}`);
},
getObligations: (params?: { status?: string; category?: string }) =>
request<Obligation[]>(`/obligations${buildQs(params)}`),
getCalendar: (start: string, end: string) =>
request<Obligation[]>(`/obligations/calendar?start=${start}&end=${end}`),
createObligation: (data: Partial<Obligation>) =>
Expand Down Expand Up @@ -325,12 +331,8 @@ export const api = {
}),

// Tasks
getTasks: (params?: { status?: string; task_type?: string; source?: string; limit?: number; offset?: number }) => {
const qs = new URLSearchParams(
Object.fromEntries(Object.entries(params || {}).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])),
).toString();
return request<{ tasks: Task[]; total: number; limit: number; offset: number }>(`/tasks${qs ? '?' + qs : ''}`);
},
getTasks: (params?: { status?: string; task_type?: string; source?: string; limit?: number; offset?: number }) =>
request<{ tasks: Task[]; total: number; limit: number; offset: number }>(`/tasks${buildQs(params)}`),
getTask: (id: string) =>
request<{ task: Task; actions: TaskAction[] }>(`/tasks/${id}`),
updateTaskStatus: (id: string, status: string, notes?: string) =>
Expand Down Expand Up @@ -360,18 +362,14 @@ export const api = {
request<LegalDeadline>(`/legal/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),

// Evidence Timeline
getCaseTimeline: (caseId: string, params?: { start?: string; end?: string }) => {
const qs = new URLSearchParams(
Object.fromEntries(Object.entries(params || {}).filter(([, v]) => v != null)),
).toString();
return request<TimelineResponse>(`/cases/${caseId}/timeline${qs ? '?' + qs : ''}`);
},
getCaseTimeline: (caseId: string, params?: { start?: string; end?: string }) =>
request<TimelineResponse>(`/cases/${encodeURIComponent(caseId)}/timeline${buildQs(params)}`),
getCaseFacts: (caseId: string) =>
request<{ caseId: string; facts: TimelineFact[] }>(`/cases/${caseId}/facts`),
request<{ caseId: string; facts: TimelineFact[] }>(`/cases/${encodeURIComponent(caseId)}/facts`),
getCaseContradictions: (caseId: string) =>
request<{ caseId: string; contradictions: Contradiction[] }>(`/cases/${caseId}/contradictions`),
request<{ caseId: string; contradictions: Contradiction[] }>(`/cases/${encodeURIComponent(caseId)}/contradictions`),
getPendingFacts: (caseId: string, limit?: number) =>
request<{ caseId: string; pending: TimelineFact[] }>(`/cases/${caseId}/pending-facts${limit ? '?limit=' + limit : ''}`),
request<{ caseId: string; pending: TimelineFact[] }>(`/cases/${encodeURIComponent(caseId)}/pending-facts${limit ? '?limit=' + limit : ''}`),

// Litigation Assistant
litigationSynthesize: (data: { rawNotes: string; property?: string; caseNumber?: string }) =>
Expand Down
9 changes: 7 additions & 2 deletions ui/src/pages/Accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { Card } from '../components/ui/Card';
import { ActionButton } from '../components/ui/ActionButton';
import { formatCurrency, formatDate, cn } from '../lib/utils';
import { useToast } from '../lib/toast';
import { ChevronDown, ChevronUp, RefreshCw, ArrowDownLeft, ArrowUpRight } from 'lucide-react';
import { ChevronDown, ChevronUp, ArrowDownLeft, ArrowUpRight } from 'lucide-react';

export function Accounts() {
const [accounts, setAccounts] = useState<Account[]>([]);
const [error, setError] = useState<string | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [txLoading, setTxLoading] = useState(false);
const [txError, setTxError] = useState<string | null>(null);
const [syncing, setSyncing] = useState(false);
const toast = useToast();

Expand All @@ -27,11 +28,13 @@ export function Accounts() {
}
setExpandedId(id);
setTxLoading(true);
setTxError(null);
try {
const data = await api.getAccount(id);
setTransactions(data.transactions || []);
} catch {
} catch (e: unknown) {
setTransactions([]);
setTxError(e instanceof Error ? e.message : 'Failed to load transactions');
} finally {
setTxLoading(false);
}
Expand Down Expand Up @@ -129,6 +132,8 @@ export function Accounts() {
<div className="ml-4 border-l-2 border-card-border pl-4 mt-1 mb-2">
{txLoading ? (
<p className="text-card-muted text-sm py-4">Loading transactions...</p>
) : txError ? (
<p className="text-urgency-red text-sm py-4">Failed to load transactions: {txError}</p>
) : transactions.length === 0 ? (
<p className="text-card-muted text-sm py-4">No recent transactions</p>
) : (
Expand Down
13 changes: 11 additions & 2 deletions ui/src/pages/ActionQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,17 @@ export function ActionQueue() {
}
}, []);

const [historyError, setHistoryError] = useState(false);

const loadHistory = useCallback(async () => {
setHistoryLoading(true);
setHistoryError(false);
try {
const data = await api.getQueueHistory(50);
setHistory(data);
} catch {
// History is non-critical
} catch (e: unknown) {
console.error('[ActionQueue] history load failed:', e);
setHistoryError(true);
} finally {
setHistoryLoading(false);
}
Expand Down Expand Up @@ -267,6 +271,11 @@ export function ActionQueue() {
<div className="space-y-2">
{historyLoading ? (
<p className="text-card-muted text-center py-8">Loading history...</p>
) : historyError ? (
<Card className="text-center py-8">
<p className="text-urgency-red">Failed to load decision history.</p>
<ActionButton label="Retry" variant="secondary" onClick={loadHistory} className="mt-2" />
</Card>
) : history.length === 0 ? (
<Card className="text-center py-8">
<p className="text-card-muted">No decision history yet.</p>
Expand Down
16 changes: 13 additions & 3 deletions ui/src/pages/Evidence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@ export function Evidence() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const [contradictionError, setContradictionError] = useState(false);

const loadTimeline = useCallback(async () => {
if (!caseId.trim()) return;
setLoading(true);
setError(null);
setTimeline(null);
setContradictions([]);
setContradictionError(false);
try {
const [tl, ctr] = await Promise.all([
api.getCaseTimeline(caseId),
api.getCaseContradictions(caseId).catch(() => ({ caseId, contradictions: [] })),
api.getCaseContradictions(caseId).catch(() => {
setContradictionError(true);
return { caseId, contradictions: [] as Contradiction[] };
}),
]);
setTimeline(tl);
setContradictions(ctr.contradictions);
Expand Down Expand Up @@ -87,9 +95,11 @@ export function Evidence() {
<MetricCard label="Contradictions" value={String(contradictions.length)} valueClassName={contradictions.length > 0 ? 'text-urgency-red' : 'text-urgency-green'} />
</div>

{timeline.warnings && timeline.warnings.length > 0 && (
{(timeline.partial || contradictionError || (timeline.warnings && timeline.warnings.length > 0)) && (
<div className="bg-amber-500/10 border border-amber-500/30 rounded-xl p-3 text-amber-400 text-sm">
{timeline.warnings.map((w, i) => <p key={i}>{w}</p>)}
{timeline.partial && <p>Warning: Timeline data may be incomplete — some sources returned partial results.</p>}
{contradictionError && <p>Warning: Contradictions could not be loaded — data may be incomplete.</p>}
{timeline.warnings?.map((w, i) => <p key={i}>{w}</p>)}
</div>
)}

Expand Down
1 change: 1 addition & 0 deletions ui/src/pages/Legal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function Legal() {
});

const load = () => {
setError(null);
api.getLegalDeadlines().then(setDeadlines).catch((e) => setError(e.message));
};

Expand Down
17 changes: 13 additions & 4 deletions ui/src/pages/LitigationAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,33 @@ export function LitigationAssistant() {
const synthesisRef = useRef<HTMLDivElement>(null);
const draftRef = useRef<HTMLDivElement>(null);

const [disputeLoadError, setDisputeLoadError] = useState(false);
const prePopulated = useRef(false);

// Load disputes for the picker
useEffect(() => {
api.getDisputes().then(setDisputes).catch(() => {});
api.getDisputes()
.then(setDisputes)
.catch(() => setDisputeLoadError(true));
}, []);

// Pre-populate from dispute context if URL has ?dispute=ID
// Pre-populate from dispute context if URL has ?dispute=ID (once only)
useEffect(() => {
if (prePopulated.current) return;
const params = new URLSearchParams(window.location.search);
const disputeId = params.get('dispute');
if (disputeId && disputes.length > 0) {
prePopulated.current = true;
setSelectedDisputeId(disputeId);
const d = disputes.find(dd => dd.id === disputeId);
if (d) {
if (d.counterparty) setRecipient(d.counterparty);
if (d.description) setRawNotes(prev => prev || d.description || '');
} else {
toast.error('Dispute not found', `Linked dispute ${disputeId.slice(0, 8)}... was not found`);
}
}
}, [disputes]);
}, [disputes, toast]);

const saveToDispute = async () => {
if (!selectedDisputeId || !draft) return;
Expand Down Expand Up @@ -356,7 +365,7 @@ export function LitigationAssistant() {
onChange={(e) => setSelectedDisputeId(e.target.value)}
className="flex-1 px-2 py-1.5 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-xs focus:outline-none"
>
<option value="">Link to dispute...</option>
<option value="">{disputeLoadError ? 'Failed to load disputes' : 'Link to dispute...'}</option>
{disputes.map((d) => (
<option key={d.id} value={d.id}>
{d.title} ({d.counterparty})
Expand Down
17 changes: 5 additions & 12 deletions ui/src/pages/Recommendations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MetricCard } from '../components/ui/MetricCard';
import { ActionButton } from '../components/ui/ActionButton';
import { formatCurrency, cn } from '../lib/utils';
import { useToast } from '../lib/toast';
import { Calendar, Mail, Globe, Clock, X } from 'lucide-react';
import { X } from 'lucide-react';

type FollowThrough = {
recId: string;
Expand Down Expand Up @@ -63,10 +63,11 @@ export function Recommendations() {

try {
await api.actOnRecommendation(id, { action_taken: action });
setRecs(recs.filter(r => r.id !== id));
setRecs(prev => prev.filter(r => r.id !== id));
toast.success('Action taken', `Recommendation marked as ${action}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Action failed';
console.error('[Recommendations] act failed:', msg, e);
setError(msg);
}
};
Expand All @@ -79,7 +80,7 @@ export function Recommendations() {

try {
await api.actOnRecommendation(followThrough.recId, { action_taken: action });
setRecs(recs.filter(r => r.id !== followThrough.recId));
setRecs(prev => prev.filter(r => r.id !== followThrough.recId));
toast.success('Action completed', `${followThrough.type} action recorded`);
setFollowThrough(null);
setDeferDate('');
Expand All @@ -94,6 +95,7 @@ export function Recommendations() {
setRecs(recs.filter(r => r.id !== id));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Dismiss failed';
console.error('[Recommendations] dismiss failed:', msg, e);
setError(msg);
}
};
Expand Down Expand Up @@ -127,14 +129,6 @@ export function Recommendations() {
return labels[type || ''] || 'Act';
};

const actionIcon = (type: string | null) => {
if (type === 'defer') return Calendar;
if (type === 'send_email') return Mail;
if (type === 'execute_browser') return Globe;
if (type === 'negotiate') return Clock;
return null;
};

const activeRec = followThrough ? recs.find(r => r.id === followThrough.recId) : null;

return (
Expand Down Expand Up @@ -232,7 +226,6 @@ export function Recommendations() {
) : (
<div className="space-y-2">
{recs.map((rec) => {
const Icon = actionIcon(rec.action_type);
return (
<Card key={rec.id} urgency={rec.priority <= 2 ? 'amber' : rec.priority <= 3 ? 'green' : null}>
<div className="space-y-3">
Expand Down
2 changes: 1 addition & 1 deletion ui/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/layout.tsx","./src/components/plaidlink.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/pages/accounts.tsx","./src/pages/bills.tsx","./src/pages/cashflow.tsx","./src/pages/dashboard.tsx","./src/pages/disputes.tsx","./src/pages/legal.tsx","./src/pages/recommendations.tsx","./src/pages/settings.tsx","./src/pages/upload.tsx"],"version":"5.9.3"}
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/ChatSidebar.tsx","./src/components/Layout.tsx","./src/components/MobileNav.tsx","./src/components/PlaidLink.tsx","./src/components/Sidebar.tsx","./src/components/StatusBar.tsx","./src/components/command/ActionStream.tsx","./src/components/command/SystemPulse.tsx","./src/components/command/VitalSigns.tsx","./src/components/dashboard/DeadlinesWidget.tsx","./src/components/dashboard/DisputesWidget.tsx","./src/components/dashboard/FocusView.tsx","./src/components/dashboard/FullView.tsx","./src/components/dashboard/ObligationsWidget.tsx","./src/components/dashboard/RecommendationsWidget.tsx","./src/components/planner/PaymentPlanView.tsx","./src/components/planner/RevenueSources.tsx","./src/components/planner/ScenarioOverride.tsx","./src/components/planner/StrategySelector.tsx","./src/components/swipe/DesktopControls.tsx","./src/components/swipe/SwipeCard.tsx","./src/components/swipe/SwipeStack.tsx","./src/components/swipe/SwipeStatsBar.tsx","./src/components/ui/ActionButton.tsx","./src/components/ui/Card.tsx","./src/components/ui/ConfirmDialog.tsx","./src/components/ui/FreshnessDot.tsx","./src/components/ui/MetricCard.tsx","./src/components/ui/ProgressDots.tsx","./src/components/ui/Skeleton.tsx","./src/components/ui/Toast.tsx","./src/components/ui/UrgencyBorder.ts","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useSwipeGesture.ts","./src/lib/api.ts","./src/lib/auth.ts","./src/lib/focus-mode.tsx","./src/lib/toast.tsx","./src/lib/utils.ts","./src/pages/Accounts.tsx","./src/pages/ActionQueue.tsx","./src/pages/Bills.tsx","./src/pages/CashFlow.tsx","./src/pages/Dashboard.tsx","./src/pages/Disputes.tsx","./src/pages/Evidence.tsx","./src/pages/Legal.tsx","./src/pages/LitigationAssistant.tsx","./src/pages/Login.tsx","./src/pages/Recommendations.tsx","./src/pages/Settings.tsx","./src/pages/Tasks.tsx","./src/pages/Upload.tsx"],"version":"5.9.3"}
Loading