Á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
Files changed (1) hide show
  1. frontend/components/AgentChat.tsx +75 -18
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
- const assistantMsg: Message = {
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/5">
86
  <div className="flex items-center gap-4">
87
- <div className="text-3xl">{selectedAgent.avatar}</div>
 
 
 
 
 
88
  <div>
89
- <h4 className="text-white font-bold">{selectedAgent.name}</h4>
 
 
 
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-xs text-white focus:outline-none focus:ring-1 focus:ring-purple-500"
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-xs text-white focus:outline-none focus:ring-1 focus:ring-purple-500"
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-slate-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
145
- <div className="w-1.5 h-1.5 bg-slate-500 rounded-full animate-bounce" style={{ animationDelay: '200ms' }} />
146
- <div className="w-1.5 h-1.5 bg-slate-500 rounded-full animate-bounce" style={{ animationDelay: '400ms' }} />
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">