Álvaro Valenzuela Valdes commited on
Commit ·
ef0e7f7
1
Parent(s): dc78036
Enhance Agent Chat with typing effects, suggested questions, and premium visual feedback
Browse files
frontend/components/AgentChat.tsx
CHANGED
|
@@ -32,8 +32,40 @@ export default function AgentChat({ tender, companyProfile }: Props) {
|
|
| 32 |
const [selectedAgent, setSelectedAgent] = useState(agents[0]);
|
| 33 |
const [selectedModel, setSelectedModel] = useState(models[0]);
|
| 34 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
| 35 |
const scrollRef = useRef<HTMLDivElement>(null);
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
useEffect(() => {
|
| 38 |
if (scrollRef.current) {
|
| 39 |
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
@@ -65,12 +97,7 @@ export default function AgentChat({ tender, companyProfile }: Props) {
|
|
| 65 |
if (!response.ok) throw new Error("Failed to chat");
|
| 66 |
|
| 67 |
const data = await response.json();
|
| 68 |
-
|
| 69 |
-
role: "assistant",
|
| 70 |
-
content: data.response,
|
| 71 |
-
agent: selectedAgent.name
|
| 72 |
-
};
|
| 73 |
-
setMessages(prev => [...prev, assistantMsg]);
|
| 74 |
} catch (error) {
|
| 75 |
console.error(error);
|
| 76 |
setMessages(prev => [...prev, { role: "assistant", content: "⚠️ Error connecting to the agent. Please try again." }]);
|
|
@@ -79,14 +106,26 @@ export default function AgentChat({ tender, companyProfile }: Props) {
|
|
| 79 |
}
|
| 80 |
};
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
return (
|
| 83 |
<div className="flex flex-col h-[600px] glass-card rounded-[2rem] overflow-hidden border border-white/10 bg-slate-900/60 backdrop-blur-xl">
|
| 84 |
{/* Chat Header */}
|
| 85 |
-
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/
|
| 86 |
<div className="flex items-center gap-4">
|
| 87 |
-
<div className=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
<div>
|
| 89 |
-
<h4 className="text-white font-bold">
|
|
|
|
|
|
|
|
|
|
| 90 |
<p className="text-[10px] text-slate-500 uppercase tracking-widest font-black">Expert Consultant</p>
|
| 91 |
</div>
|
| 92 |
</div>
|
|
@@ -94,16 +133,17 @@ export default function AgentChat({ tender, companyProfile }: Props) {
|
|
| 94 |
<select
|
| 95 |
value={selectedAgent.id}
|
| 96 |
onChange={(e) => setSelectedAgent(agents.find(a => a.id === e.target.value) || agents[0])}
|
| 97 |
-
className="bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-
|
| 98 |
>
|
| 99 |
-
{agents.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
|
| 100 |
</select>
|
|
|
|
| 101 |
<select
|
| 102 |
value={selectedModel}
|
| 103 |
onChange={(e) => setSelectedModel(e.target.value)}
|
| 104 |
-
className="bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-
|
| 105 |
>
|
| 106 |
-
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
| 107 |
</select>
|
| 108 |
</div>
|
| 109 |
</div>
|
|
@@ -137,19 +177,36 @@ export default function AgentChat({ tender, companyProfile }: Props) {
|
|
| 137 |
</div>
|
| 138 |
</div>
|
| 139 |
))}
|
| 140 |
-
{isLoading && (
|
| 141 |
<div className="flex justify-start">
|
| 142 |
<div className="bg-white/5 rounded-2xl rounded-tl-none px-5 py-3 border border-white/10">
|
| 143 |
-
<div className="flex gap-1">
|
| 144 |
-
<div className="w-1.5 h-1.5 bg-
|
| 145 |
-
<div className="w-1.5 h-1.5 bg-
|
| 146 |
-
<div className="w-1.5 h-1.5 bg-
|
| 147 |
</div>
|
| 148 |
</div>
|
| 149 |
</div>
|
| 150 |
)}
|
| 151 |
</div>
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
{/* Input Area */}
|
| 154 |
<div className="p-6 bg-white/5 border-t border-white/5">
|
| 155 |
<div className="flex gap-3">
|
|
|
|
| 32 |
const [selectedAgent, setSelectedAgent] = useState(agents[0]);
|
| 33 |
const [selectedModel, setSelectedModel] = useState(models[0]);
|
| 34 |
const [isLoading, setIsLoading] = useState(false);
|
| 35 |
+
const [isTyping, setIsTyping] = useState(false);
|
| 36 |
const scrollRef = useRef<HTMLDivElement>(null);
|
| 37 |
|
| 38 |
+
const suggestedQuestions = [
|
| 39 |
+
"Summarize the main requirements",
|
| 40 |
+
"Identify legal risks for my company",
|
| 41 |
+
"How does my experience fit here?",
|
| 42 |
+
"Generate a technical summary",
|
| 43 |
+
];
|
| 44 |
+
|
| 45 |
+
const simulateTyping = (text: string, agentName: string) => {
|
| 46 |
+
setIsTyping(true);
|
| 47 |
+
let currentText = "";
|
| 48 |
+
const words = text.split(" ");
|
| 49 |
+
let i = 0;
|
| 50 |
+
|
| 51 |
+
const interval = setInterval(() => {
|
| 52 |
+
if (i < words.length) {
|
| 53 |
+
currentText += (i === 0 ? "" : " ") + words[i];
|
| 54 |
+
setMessages(prev => {
|
| 55 |
+
const last = prev[prev.length - 1];
|
| 56 |
+
if (last && last.role === 'assistant' && last.agent === agentName) {
|
| 57 |
+
return [...prev.slice(0, -1), { ...last, content: currentText }];
|
| 58 |
+
}
|
| 59 |
+
return [...prev, { role: 'assistant', content: currentText, agent: agentName }];
|
| 60 |
+
});
|
| 61 |
+
i++;
|
| 62 |
+
} else {
|
| 63 |
+
clearInterval(interval);
|
| 64 |
+
setIsTyping(false);
|
| 65 |
+
}
|
| 66 |
+
}, 40);
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
useEffect(() => {
|
| 70 |
if (scrollRef.current) {
|
| 71 |
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
|
|
| 97 |
if (!response.ok) throw new Error("Failed to chat");
|
| 98 |
|
| 99 |
const data = await response.json();
|
| 100 |
+
simulateTyping(data.response, selectedAgent.name);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
} catch (error) {
|
| 102 |
console.error(error);
|
| 103 |
setMessages(prev => [...prev, { role: "assistant", content: "⚠️ Error connecting to the agent. Please try again." }]);
|
|
|
|
| 106 |
}
|
| 107 |
};
|
| 108 |
|
| 109 |
+
const handleSuggestedClick = (question: string) => {
|
| 110 |
+
setInput(question);
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
return (
|
| 114 |
<div className="flex flex-col h-[600px] glass-card rounded-[2rem] overflow-hidden border border-white/10 bg-slate-900/60 backdrop-blur-xl">
|
| 115 |
{/* Chat Header */}
|
| 116 |
+
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.03]">
|
| 117 |
<div className="flex items-center gap-4">
|
| 118 |
+
<div className={`text-3xl transition-all duration-500 relative ${isLoading || isTyping ? 'scale-110' : ''}`}>
|
| 119 |
+
{selectedAgent.avatar}
|
| 120 |
+
{(isLoading || isTyping) && (
|
| 121 |
+
<div className="absolute inset-0 bg-purple-500/20 blur-xl rounded-full animate-pulse" />
|
| 122 |
+
)}
|
| 123 |
+
</div>
|
| 124 |
<div>
|
| 125 |
+
<h4 className="text-white font-bold flex items-center gap-2">
|
| 126 |
+
{selectedAgent.name}
|
| 127 |
+
{(isLoading || isTyping) && <span className="h-1.5 w-1.5 bg-green-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.8)]" />}
|
| 128 |
+
</h4>
|
| 129 |
<p className="text-[10px] text-slate-500 uppercase tracking-widest font-black">Expert Consultant</p>
|
| 130 |
</div>
|
| 131 |
</div>
|
|
|
|
| 133 |
<select
|
| 134 |
value={selectedAgent.id}
|
| 135 |
onChange={(e) => setSelectedAgent(agents.find(a => a.id === e.target.value) || agents[0])}
|
| 136 |
+
className="bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-[10px] uppercase font-black tracking-widest text-slate-400 hover:text-white transition-all cursor-pointer outline-none focus:border-purple-500/50"
|
| 137 |
>
|
| 138 |
+
{agents.map(a => <option key={a.id} value={a.id} className="bg-slate-900">{a.name}</option>)}
|
| 139 |
</select>
|
| 140 |
+
<div className="h-6 w-px bg-white/5" />
|
| 141 |
<select
|
| 142 |
value={selectedModel}
|
| 143 |
onChange={(e) => setSelectedModel(e.target.value)}
|
| 144 |
+
className="bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-[10px] uppercase font-black tracking-widest text-slate-400 hover:text-white transition-all cursor-pointer outline-none focus:border-purple-500/50"
|
| 145 |
>
|
| 146 |
+
{models.map(m => <option key={m} value={m} className="bg-slate-900">{m}</option>)}
|
| 147 |
</select>
|
| 148 |
</div>
|
| 149 |
</div>
|
|
|
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
))}
|
| 180 |
+
{isLoading && !isTyping && (
|
| 181 |
<div className="flex justify-start">
|
| 182 |
<div className="bg-white/5 rounded-2xl rounded-tl-none px-5 py-3 border border-white/10">
|
| 183 |
+
<div className="flex gap-1.5">
|
| 184 |
+
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
| 185 |
+
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '200ms' }} />
|
| 186 |
+
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '400ms' }} />
|
| 187 |
</div>
|
| 188 |
</div>
|
| 189 |
</div>
|
| 190 |
)}
|
| 191 |
</div>
|
| 192 |
|
| 193 |
+
{/* Suggested Questions */}
|
| 194 |
+
{messages.length < 3 && !isLoading && !isTyping && (
|
| 195 |
+
<div className="px-6 pb-4 bg-transparent overflow-x-auto no-scrollbar">
|
| 196 |
+
<div className="flex gap-2 whitespace-nowrap">
|
| 197 |
+
{suggestedQuestions.map((q, i) => (
|
| 198 |
+
<button
|
| 199 |
+
key={i}
|
| 200 |
+
onClick={() => handleSuggestedClick(q)}
|
| 201 |
+
className="bg-white/5 border border-white/10 rounded-full px-4 py-2 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 hover:border-purple-500/50 transition-all active:scale-95"
|
| 202 |
+
>
|
| 203 |
+
{q}
|
| 204 |
+
</button>
|
| 205 |
+
))}
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
)}
|
| 209 |
+
|
| 210 |
{/* Input Area */}
|
| 211 |
<div className="p-6 bg-white/5 border-t border-white/5">
|
| 212 |
<div className="flex gap-3">
|