wulf-pulse/components/analyzer/share-modal.tsx
lorentz 8f8b5ab7be feat: AI ticket analyzer (phases 1-6)
Multi-stage LLM pipeline that produces structured analyses of Autotask
tickets from local Postgres. Migration 069 + Zod schemas, Stage 0
preprocessor, IT Glue redaction + search, Anthropic SDK wrapper, Stages
1/3/4 (Haiku/Sonnet/Opus), pipeline + cost circuit breaker, job worker
(opt-in autostart), 6 API routes, 3 frontend pages, share-row
persistence (email send deferred to phase 7). 128 vitest tests, tsc
clean. Build journal in docs/wulf-pulse-ticket-analyzer-build-notes.md.

Sync: adds syncTicketNotes() + ticket_notes to ordered/date-filtered
entities so the analyzer's local mirror stays current via scheduler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:59:40 -04:00

114 lines
3.4 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Share2 } from 'lucide-react';
import { toast } from 'sonner';
interface ShareModalProps {
analysisId: string;
}
export function ShareModal({ analysisId }: ShareModalProps) {
const [open, setOpen] = useState(false);
const [recipientEmail, setRecipientEmail] = useState('');
const [note, setNote] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitting(true);
try {
const res = await fetch(`/api/analyzer/analyses/${analysisId}/share`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipientEmail,
note: note.trim() || undefined,
}),
});
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as { error?: string; message?: string };
throw new Error(data.message ?? data.error ?? `Request failed: ${res.status}`);
}
toast.success(`Shared with ${recipientEmail}`);
setOpen(false);
setRecipientEmail('');
setNote('');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Unknown error';
toast.error(msg);
} finally {
setSubmitting(false);
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Share2 className="w-4 h-4 mr-2" />
Share
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Share this analysis</DialogTitle>
<DialogDescription>
Recipient must be on an allowed domain (set via
ALLOWED_SHARE_DOMAINS).
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="recipient">Recipient email</Label>
<Input
id="recipient"
type="email"
required
value={recipientEmail}
onChange={(e) => setRecipientEmail(e.target.value)}
placeholder="colleague@wulfconsulting.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="note">Note (optional)</Label>
<Textarea
id="note"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Why you're sharing this…"
rows={3}
maxLength={2000}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={submitting}
>
Cancel
</Button>
<Button type="submit" disabled={submitting || !recipientEmail}>
{submitting ? 'Sharing…' : 'Share'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}