agent-card.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. "use client"
  2. import type { Agent } from "@/types/agent"
  3. import { Card, CardContent, CardHeader } from "@/components/ui/card"
  4. import { Badge } from "@/components/ui/badge"
  5. import { Button } from "@/components/ui/button"
  6. import { MessageSquare, Info } from "lucide-react"
  7. import Image from "next/image"
  8. import { motion } from "framer-motion"
  9. import Link from "next/link"
  10. import { useDiagnosisRunner } from "@/hooks/use-diagnosis"
  11. import { cn } from "@/lib/utils"
  12. import { useState } from "react"
  13. import { forwardRef, useImperativeHandle } from "react";
  14. interface AgentCardProps {
  15. agent: Agent
  16. onUpdateAgent?: (agent: Agent) => void
  17. index: number
  18. }
  19. export interface AgentCardHandle {
  20. runDiagnosis: (signal: AbortSignal) => Promise<void>;
  21. }
  22. const AMICA_URL = process.env.NEXT_PUBLIC_AMICA_URL as string;
  23. export const AgentCard = forwardRef<AgentCardHandle, AgentCardProps>(({ agent, onUpdateAgent, index }, ref) => {
  24. const { status, checking, handleDiagnosis } = useDiagnosisRunner(agent, index, true);
  25. const [diagnosed, setDiagnosed] = useState(false);
  26. const runDiagnosis = async (signal: AbortSignal): Promise<void> => {
  27. return new Promise<void>(async (resolve, reject) => {
  28. if (signal.aborted) {
  29. return reject(new DOMException("Aborted", "AbortError"));
  30. }
  31. signal.addEventListener("abort", () => {
  32. console.log("Diagnosis aborted");
  33. reject(new DOMException("Aborted", "AbortError"));
  34. });
  35. setDiagnosed(true);
  36. await handleDiagnosis(false);
  37. resolve();
  38. });
  39. };
  40. useImperativeHandle(ref, () => ({
  41. runDiagnosis,
  42. }));
  43. return (
  44. <motion.div
  45. initial={{ opacity: 0, y: 20 }}
  46. animate={{ opacity: 1, y: 0 }}
  47. transition={{ duration: 0.5, delay: index * 0.1 }}
  48. whileHover={{ y: -5 }}
  49. className="h-full"
  50. >
  51. <Card className="overflow-hidden group bg-scifi-dark border-neon-blue/20 hover:border-neon-blue/40 transition-colors h-full flex flex-col">
  52. <CardHeader className="p-0">
  53. <div className="relative h-[320px] w-full overflow-hidden">
  54. <motion.div whileHover={{ scale: 1.05 }} transition={{ duration: 0.4 }} className=" relative h-full w-full">
  55. <Image
  56. src={agent.avatar || "/placeholder.svg"}
  57. alt={agent.name}
  58. className="object-cover object-center"
  59. fill
  60. priority
  61. />
  62. </motion.div>
  63. </div>
  64. </CardHeader>
  65. <CardContent className="p-4 flex-grow flex flex-col justify-between space-y-4">
  66. <div className="space-y-2">
  67. <div className="flex items-center justify-between">
  68. <h3 className="font-orbitron font-semibold text-lg text-white">{agent.name}</h3>
  69. <div className="flex items-center gap-2">
  70. <span className="text-sm font-medium text-neon-pink font-orbitron">{agent.price?.toPrecision(2)} AIUS</span>
  71. <Badge
  72. variant="secondary"
  73. loading={checking}
  74. className={cn(
  75. "bg-neon-blue border-0 text-white font-roboto-mono",
  76. "transition-all duration-300 ease-in-out",
  77. "hover:bg-neon-blue/90 hover:shadow-[0_0_10px_rgba(0,245,255,0.6)] hover:scale-[1.03]",
  78. "active:scale-[0.98]",
  79. "focus-visible:ring-2 focus-visible:ring-neon-blue/50"
  80. )}
  81. tabIndex={0}
  82. >
  83. {checking ? "loading.." : diagnosed ? status : agent.status}
  84. </Badge>
  85. </div>
  86. </div>
  87. <div className="space-y-1">
  88. <p className="text-sm text-blue-100/70 font-roboto-mono">Token: {agent.token}</p>
  89. <p className="text-sm text-blue-100/70 font-roboto-mono">
  90. Tier: {agent.tier?.name} (Level {agent.tier?.level})
  91. </p>
  92. </div>
  93. {/* Scrollable Description */}
  94. <div className="max-h-24 overflow-y-auto pr-1 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar]:h-4/6 [&::-webkit-scrollbar-track]:bg-neon-blue/25 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300">
  95. <p className="text-sm text-blue-100/70 font-roboto-mono whitespace-pre-line">
  96. {agent.description}
  97. </p>
  98. </div>
  99. </div>
  100. <div className="grid grid-cols-2 gap-2">
  101. <Button
  102. variant="outline"
  103. size="sm"
  104. className="w-full font-roboto-mono border-neon-blue/50 text-neon-blue hover:bg-neon-blue/20 hover:text-white transition-colors"
  105. onClick={() => window.open(`${AMICA_URL}/agent/${agent.id}`, "_blank", "noopener,noreferrer")}
  106. disabled={diagnosed ? status !== "active" : agent.status !== "active"}
  107. title={
  108. (diagnosed ? status !== "active" : agent.status !== "active")
  109. ? "Chat is disabled: Agent is inactive."
  110. : ""
  111. }
  112. >
  113. <MessageSquare className="h-4 w-4 mr-2" />
  114. Chat
  115. </Button>
  116. <Link href={`/agent/${agent.agentId}`}>
  117. <Button
  118. variant="outline"
  119. size="sm"
  120. className="w-full font-roboto-mono border-neon-pink/50 text-neon-pink hover:bg-neon-pink/20 hover:text-white transition-colors"
  121. >
  122. <Info className="h-4 w-4 mr-2" />
  123. Details
  124. </Button>
  125. </Link>
  126. </div>
  127. </CardContent>
  128. </Card>
  129. </motion.div>
  130. )
  131. }
  132. )
  133. AgentCard.displayName = "AgentCard"