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
6 changes: 6 additions & 0 deletions apps/web/src/app/api/copilot/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ ${signalsContext}`;
});

const data = await res.json();
if (!res.ok || data?.type === 'error') {
console.error('Anthropic API error:', JSON.stringify(data));
return NextResponse.json({
reply: `Iris error: ${data?.error?.message ?? data?.error?.type ?? 'Unknown error from Anthropic API'}`,
});
}
const reply = data?.content?.[0]?.text ?? 'No response.';
return NextResponse.json({ reply });
}
12 changes: 9 additions & 3 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ export default async function HomePage({ searchParams }: PageProps) {
const hospitalMap = Object.fromEntries(hospitals.map((h) => [h.id, h.name]));

// Strip garbage — filtered_out signals should never appear in the feed
const cleanSignals = allSignals.filter(
(s) => s.tier !== 'filtered_out' && s.signal_type !== 'filtered_out'
);
const seenUrls = new Set<string>();
const cleanSignals = allSignals.filter((s) => {
if (s.tier === 'filtered_out' || s.signal_type === 'filtered_out') return false;
if (s.source_url) {
if (seenUrls.has(s.source_url)) return false;
seenUrls.add(s.source_url);
}
return true;
});

// Filter by category
let signals = category
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/review/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default async function ReviewPage() {
return (
<div className="px-4 py-5 md:px-8 md:py-7 pb-20 md:pb-7">
<header className="mb-6">
<h1 className="font-serif text-2xl font-semibold text-brand">Review queue</h1>
<h1 className="font-serif text-2xl font-semibold text-brand">Review Queue</h1>
<p className="text-sm text-slate-500 mt-1">
Low-confidence signals (under 70%) awaiting Danielle&apos;s approval before digest.
{signals.length > 0 && (
Expand Down
51 changes: 31 additions & 20 deletions apps/web/src/components/CoPilot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function IrisMessage({ text }: { text: string }) {
const lines = text.split('\n');
const nodes: React.ReactNode[] = [];
let i = 0;
let k = 0;

while (i < lines.length) {
const line = lines[i] ?? '';
Expand All @@ -31,7 +32,7 @@ function IrisMessage({ text }: { text: string }) {
i++;
}
nodes.push(
<ul key={i} className="list-disc list-outside pl-4 space-y-0.5 my-1">
<ul key={k++} className="list-disc list-outside pl-4 space-y-0.5 my-1">
{items.map((item, j) => (
<li key={j}>{renderInline(item)}</li>
))}
Expand All @@ -48,7 +49,7 @@ function IrisMessage({ text }: { text: string }) {
i++;
}
nodes.push(
<ol key={i} className="list-decimal list-outside pl-4 space-y-0.5 my-1">
<ol key={k++} className="list-decimal list-outside pl-4 space-y-0.5 my-1">
{items.map((item, j) => (
<li key={j}>{renderInline(item)}</li>
))}
Expand All @@ -59,7 +60,7 @@ function IrisMessage({ text }: { text: string }) {

// Regular paragraph
nodes.push(
<p key={i} className="my-0.5">
<p key={k++} className="my-0.5">
{renderInline(line)}
</p>
);
Expand Down Expand Up @@ -131,7 +132,7 @@ function defaultPos() {
const isMobile = window.innerWidth < 768;
return {
x: window.innerWidth - BUBBLE_SIZE - (isMobile ? 16 : 24),
y: window.innerHeight - BUBBLE_SIZE - (isMobile ? 88 : 24),
y: window.innerHeight - BUBBLE_SIZE - (isMobile ? 96 : 48),
};
}

Expand All @@ -146,7 +147,7 @@ export default function CoPilot() {
const [pulsing, setPulsing] = useState(false);
const [hasUnread, setHasUnread] = useState(false);
// null = not yet mounted (SSR safe)
const { userId, isAdmin } = useUser();
const { userId, isAdmin, userName } = useUser();
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const pillDismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
Expand All @@ -170,11 +171,11 @@ export default function CoPilot() {
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) drag.current.moved = true;
const newX = Math.max(
0,
Math.min(window.innerWidth - BUBBLE_SIZE, drag.current.startPosX + dx)
Math.min(window.innerWidth - BUBBLE_SIZE - 8, drag.current.startPosX + dx)
);
const newY = Math.max(
0,
Math.min(window.innerHeight - BUBBLE_SIZE, drag.current.startPosY + dy)
Math.min(window.innerHeight - BUBBLE_SIZE - 16, drag.current.startPosY + dy)
);
setPos({ x: newX, y: newY });
}
Expand All @@ -185,8 +186,8 @@ export default function CoPilot() {
setPos((prev) => {
if (!prev) return defaultPos();
return {
x: Math.max(0, Math.min(window.innerWidth - BUBBLE_SIZE, prev.x)),
y: Math.max(0, Math.min(window.innerHeight - BUBBLE_SIZE, prev.y)),
x: Math.max(0, Math.min(window.innerWidth - BUBBLE_SIZE - 8, prev.x)),
y: Math.max(0, Math.min(window.innerHeight - BUBBLE_SIZE - 16, prev.y)),
};
});
}
Expand Down Expand Up @@ -338,12 +339,10 @@ export default function CoPilot() {
};
}

const gap = 8;
let top = pos.y - PANEL_HEIGHT - gap;
if (top < 8) top = pos.y + BUBBLE_SIZE + gap;
let left = pos.x + BUBBLE_SIZE - PANEL_WIDTH;
left = Math.max(8, Math.min(window.innerWidth - PANEL_WIDTH - 8, left));
return { position: 'fixed', top, left, zIndex: 50, width: PANEL_WIDTH };
const gap = 12;
const bottom = window.innerHeight - pos.y + gap;
const right = Math.max(8, window.innerWidth - pos.x - BUBBLE_SIZE);
return { position: 'fixed', bottom, right, zIndex: 50, width: PANEL_WIDTH };
}

if (!pos) return null;
Expand Down Expand Up @@ -534,6 +533,11 @@ export default function CoPilot() {
>
{messages.length === 0 && (
<div className="space-y-2">
{userName && (
<p className="text-xs text-slate-500 pb-1">
Hi {userName.split(' ')[0]}, what do you need?
</p>
)}
<button
onClick={() =>
handleSend(
Expand All @@ -545,11 +549,18 @@ export default function CoPilot() {
>
Brief me on my territory
</button>
{[
'Who should I call this week?',
'Draft an outreach email for Ascension',
"What's urgent across my accounts?",
].map((prompt) => (
{(isAdmin
? [
'Draft my weekly digest',
"What's urgent across all accounts?",
'Which AEs need a follow-up this week?',
]
: [
'Who should I call this week?',
'Summarize my most important account',
"What's urgent across my accounts?",
]
).map((prompt) => (
<button
key={prompt}
onClick={() => handleSend(prompt)}
Expand Down
Loading