Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Badge } from "./ui/badge";
import { Sparkles, Loader2, AlertCircle, Lightbulb } from "lucide-react";
import { apiRequest } from "../lib/queryClient";
import type { AiInsight } from "@shared/schema";
import { useEffect, useState } from "react";
interface AiInsightsCardProps {
checkInId: string;
}
export function AiInsightsCard({ checkInId }: AiInsightsCardProps) {
const [pollCount, setPollCount] = useState(0);
const maxPolls = 15; // 15 polls * 2 seconds = 30 seconds
const { data: insights, isLoading, error } = useQuery<AiInsight[]>({
queryKey: ["/api/check-ins", checkInId, "insights"],
queryFn: async () => {
const res = await apiRequest("GET", `/api/check-ins/${checkInId}/insights`);
return res.json();
},
refetchInterval: (query) => {
// Stop polling if we have insights or reached max polls
const data = query.state.data;
if ((data && data.length > 0) || pollCount >= maxPolls) {
return false;
}
return 2000; // Poll every 2 seconds
},
refetchIntervalInBackground: false,
});
useEffect(() => {
if (!insights || insights.length === 0) {
const timer = setInterval(() => {
setPollCount((prev) => prev + 1);
}, 2000);
return () => clearInterval(timer);
}
}, [insights]);
const getSentimentColor = (sentiment: string | null) => {
switch (sentiment) {
case "positive":
return "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400";
case "negative":
return "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400";
case "mixed":
return "bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400";
default:
return "bg-gray-100 dark:bg-gray-900/30 text-gray-700 dark:text-gray-400";
}
};
const getProviderBadgeColor = (modelName: string) => {
if (modelName.includes("claude")) {
return "bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 border-purple-200";
}
if (modelName.includes("gpt")) {
return "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 border-blue-200";
}
if (modelName.includes("gemini")) {
return "bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 border-orange-200";
}
return "bg-gray-100 dark:bg-gray-900/30 text-gray-700 dark:text-gray-400 border-gray-200";
};
if (error) {
return (
<Card className="border-red-200 dark:border-red-900">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-red-700 dark:text-red-400">
<AlertCircle className="h-5 w-5" />
Unable to load AI insights
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
AI analysis is temporarily unavailable. Your check-in was saved successfully.
</p>
</CardContent>
</Card>
);
}
if (isLoading || ((!insights || insights.length === 0) && pollCount < maxPolls)) {
return (
<Card className="border-purple-200 dark:border-purple-900">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin text-purple-600 dark:text-purple-400" />
AI is analyzing your check-in...
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Testing multiple AI models to provide you with insights. This may take a few moments.
</p>
<div className="mt-4 flex gap-2">
<div className="h-2 w-2 rounded-full bg-purple-500 animate-pulse" />
<div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse delay-100" />
<div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse delay-200" />
</div>
</CardContent>
</Card>
);
}
if (!insights || insights.length === 0) {
return null; // No insights after polling timeout
}
return (
<Card className="border-purple-200 dark:border-purple-900">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-purple-600 dark:text-purple-400" />
AI Insights
</CardTitle>
<CardDescription>
Analysis from {insights.length} AI model{insights.length > 1 ? "s" : ""}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{insights.map((insight) => (
<div
key={insight.id}
className="rounded-lg border border-gray-200 dark:border-gray-800 p-4 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<Badge variant="outline" className={getProviderBadgeColor(insight.modelName)}>
{insight.modelName}
</Badge>
{insight.sentiment && (
<Badge variant="outline" className={getSentimentColor(insight.sentiment)}>
{insight.sentiment}
</Badge>
)}
</div>
<div>
<p className="text-sm leading-relaxed">{insight.insight}</p>
</div>
{insight.suggestion && (
<div className="flex gap-2 mt-3 p-3 rounded-md bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-900">
<Lightbulb className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-900 dark:text-blue-100">{insight.suggestion}</p>
</div>
)}
</div>
))}
</CardContent>
</Card>
);
}
|