Álvaro Valenzuela Valdes commited on
Commit
e3da83f
·
1 Parent(s): da1e56d

feat: responsive agent chat with file upload capability

Browse files
Files changed (1) hide show
  1. frontend/components/AgentChat.tsx +66 -24
frontend/components/AgentChat.tsx CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import { useState, useRef, useEffect } from "react";
4
  import type { Tender, CompanyProfile } from "../lib/types";
 
5
 
6
  type Message = {
7
  role: "user" | "assistant";
@@ -34,7 +35,10 @@ export default function AgentChat({ tender, companyProfile }: Props) {
34
  const [selectedModel, setSelectedModel] = useState(models[0]);
35
  const [isLoading, setIsLoading] = useState(false);
36
  const [isTyping, setIsTyping] = useState(false);
 
 
37
  const scrollRef = useRef<HTMLDivElement>(null);
 
38
 
39
  const suggestedQuestions = [
40
  "Summarize the main requirements",
@@ -74,12 +78,13 @@ export default function AgentChat({ tender, companyProfile }: Props) {
74
  }
75
  }, [messages]);
76
 
77
- const handleSend = async () => {
78
- if (!input.trim() || isLoading) return;
 
79
 
80
- const userMsg: Message = { role: "user", content: input };
81
  setMessages(prev => [...prev, userMsg]);
82
- setInput("");
83
  setIsLoading(true);
84
 
85
  try {
@@ -89,10 +94,10 @@ export default function AgentChat({ tender, companyProfile }: Props) {
89
  body: JSON.stringify({
90
  tender,
91
  company_profile: companyProfile,
92
- message: input,
93
  agent: selectedAgent.id,
94
  model: selectedModel,
95
- history: messages,
96
  }),
97
  });
98
 
@@ -108,42 +113,68 @@ export default function AgentChat({ tender, companyProfile }: Props) {
108
  }
109
  };
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  const handleSuggestedClick = (question: string) => {
112
  setInput(question);
113
  };
114
 
115
  return (
116
- <div className="flex flex-col h-[600px] glass-card rounded-[2rem] overflow-hidden border border-white/10 bg-slate-900/60 backdrop-blur-xl">
 
 
 
 
 
 
 
 
117
  {/* Chat Header */}
118
- <div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.03]">
119
  <div className="flex items-center gap-4">
120
- <div className={`text-3xl transition-all duration-500 relative ${isLoading || isTyping ? 'scale-110' : ''}`}>
121
  {selectedAgent.avatar}
122
- {(isLoading || isTyping) && (
123
  <div className="absolute inset-0 bg-purple-500/20 blur-xl rounded-full animate-pulse" />
124
  )}
125
  </div>
126
  <div>
127
- <h4 className="text-white font-bold flex items-center gap-2">
128
  {selectedAgent.name}
129
- {(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)]" />}
130
  </h4>
131
- <p className="text-[10px] text-slate-500 uppercase tracking-widest font-black">Expert Consultant</p>
132
  </div>
133
  </div>
134
- <div className="flex items-center gap-3">
135
  <select
136
  value={selectedAgent.id}
137
  onChange={(e) => setSelectedAgent(agents.find(a => a.id === e.target.value) || agents[0])}
138
- 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"
139
  >
140
  {agents.map(a => <option key={a.id} value={a.id} className="bg-slate-900">{a.name}</option>)}
141
  </select>
142
- <div className="h-6 w-px bg-white/5" />
143
  <select
144
  value={selectedModel}
145
  onChange={(e) => setSelectedModel(e.target.value)}
146
- 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"
147
  >
148
  {models.map(m => <option key={m} value={m} className="bg-slate-900">{m}</option>)}
149
  </select>
@@ -210,20 +241,31 @@ export default function AgentChat({ tender, companyProfile }: Props) {
210
  )}
211
 
212
  {/* Input Area */}
213
- <div className="p-6 bg-white/5 border-t border-white/5">
214
- <div className="flex gap-3">
 
 
 
 
 
 
 
 
 
215
  <input
216
  type="text"
217
  value={input}
218
  onChange={(e) => setInput(e.target.value)}
219
  onKeyDown={(e) => e.key === 'Enter' && handleSend()}
220
- placeholder={`Message ${selectedAgent.name}...`}
221
- className="flex-1 bg-black/40 border border-white/10 rounded-2xl px-5 py-3 text-white text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all"
 
222
  />
 
223
  <button
224
- onClick={handleSend}
225
- disabled={!input.trim() || isLoading}
226
- className="w-12 h-12 rounded-2xl premium-gradient text-white flex items-center justify-center transition-all active:scale-95 disabled:opacity-30 shadow-lg shadow-purple-500/20"
227
  >
228
  <span className="text-xl">✈️</span>
229
  </button>
 
2
 
3
  import { useState, useRef, useEffect } from "react";
4
  import type { Tender, CompanyProfile } from "../lib/types";
5
+ import { uploadDocument } from "../lib/api";
6
 
7
  type Message = {
8
  role: "user" | "assistant";
 
35
  const [selectedModel, setSelectedModel] = useState(models[0]);
36
  const [isLoading, setIsLoading] = useState(false);
37
  const [isTyping, setIsTyping] = useState(false);
38
+ const [isUploading, setIsUploading] = useState(false);
39
+ const [contextText, setContextText] = useState("");
40
  const scrollRef = useRef<HTMLDivElement>(null);
41
+ const fileInputRef = useRef<HTMLInputElement>(null);
42
 
43
  const suggestedQuestions = [
44
  "Summarize the main requirements",
 
78
  }
79
  }, [messages]);
80
 
81
+ const handleSend = async (overrideInput?: string) => {
82
+ const messageToSend = overrideInput || input;
83
+ if (!messageToSend.trim() || isLoading) return;
84
 
85
+ const userMsg: Message = { role: "user", content: messageToSend };
86
  setMessages(prev => [...prev, userMsg]);
87
+ if (!overrideInput) setInput("");
88
  setIsLoading(true);
89
 
90
  try {
 
94
  body: JSON.stringify({
95
  tender,
96
  company_profile: companyProfile,
97
+ message: contextText ? `[DOC CONTEXT: ${contextText.slice(0, 3000)}]\n\nUSER QUESTION: ${messageToSend}` : messageToSend,
98
  agent: selectedAgent.id,
99
  model: selectedModel,
100
+ history: messages.map(({role, content}) => ({role, content})),
101
  }),
102
  });
103
 
 
113
  }
114
  };
115
 
116
+ const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
117
+ if (e.target.files && e.target.files[0]) {
118
+ const file = e.target.files[0];
119
+ setIsUploading(true);
120
+ try {
121
+ const result = await uploadDocument(file);
122
+ setContextText(prev => prev + "\n" + result.text);
123
+ setMessages(prev => [...prev, { role: "user", content: `📎 Attached document: ${file.name}` }]);
124
+ simulateTyping(`He analizado el documento "${file.name}". ¿Qué te gustaría saber sobre su contenido?`, selectedAgent.name);
125
+ } catch (error) {
126
+ console.error(error);
127
+ alert("Error uploading document.");
128
+ } finally {
129
+ setIsUploading(false);
130
+ }
131
+ }
132
+ };
133
+
134
  const handleSuggestedClick = (question: string) => {
135
  setInput(question);
136
  };
137
 
138
  return (
139
+ <div className="flex flex-col h-[600px] md:h-[650px] glass-card rounded-[2rem] overflow-hidden border border-white/10 bg-slate-900/60 backdrop-blur-xl shadow-2xl transition-all duration-500">
140
+ <input
141
+ type="file"
142
+ ref={fileInputRef}
143
+ onChange={handleFileUpload}
144
+ className="hidden"
145
+ accept=".pdf,.docx,.doc,.txt"
146
+ />
147
+
148
  {/* Chat Header */}
149
+ <div className="p-4 md:p-6 border-b border-white/5 flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white/[0.03]">
150
  <div className="flex items-center gap-4">
151
+ <div className={`text-2xl md:text-3xl transition-all duration-500 relative ${isLoading || isTyping ? 'scale-110' : ''}`}>
152
  {selectedAgent.avatar}
153
+ {(isLoading || isTyping || isUploading) && (
154
  <div className="absolute inset-0 bg-purple-500/20 blur-xl rounded-full animate-pulse" />
155
  )}
156
  </div>
157
  <div>
158
+ <h4 className="text-white text-sm md:text-base font-bold flex items-center gap-2">
159
  {selectedAgent.name}
160
+ {(isLoading || isTyping || isUploading) && <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)]" />}
161
  </h4>
162
+ <p className="text-[9px] text-slate-500 uppercase tracking-widest font-black">Expert Consultant</p>
163
  </div>
164
  </div>
165
+ <div className="flex items-center gap-2 md:gap-3 overflow-x-auto no-scrollbar">
166
  <select
167
  value={selectedAgent.id}
168
  onChange={(e) => setSelectedAgent(agents.find(a => a.id === e.target.value) || agents[0])}
169
+ className="bg-white/5 border border-white/10 rounded-xl px-2 md:px-3 py-1.5 text-[9px] md:text-[10px] uppercase font-black tracking-widest text-slate-400 hover:text-white transition-all cursor-pointer outline-none focus:border-purple-500/50"
170
  >
171
  {agents.map(a => <option key={a.id} value={a.id} className="bg-slate-900">{a.name}</option>)}
172
  </select>
173
+ <div className="h-6 w-px bg-white/5 shrink-0" />
174
  <select
175
  value={selectedModel}
176
  onChange={(e) => setSelectedModel(e.target.value)}
177
+ className="bg-white/5 border border-white/10 rounded-xl px-2 md:px-3 py-1.5 text-[9px] md:text-[10px] uppercase font-black tracking-widest text-slate-400 hover:text-white transition-all cursor-pointer outline-none focus:border-purple-500/50"
178
  >
179
  {models.map(m => <option key={m} value={m} className="bg-slate-900">{m}</option>)}
180
  </select>
 
241
  )}
242
 
243
  {/* Input Area */}
244
+ <div className="p-4 md:p-6 bg-white/5 border-t border-white/5">
245
+ <div className="flex gap-2 md:gap-3 items-center">
246
+ <button
247
+ onClick={() => fileInputRef.current?.click()}
248
+ disabled={isUploading || isLoading}
249
+ className="w-10 h-10 md:w-12 md:h-12 rounded-2xl bg-white/5 border border-white/10 text-slate-400 flex items-center justify-center transition-all hover:bg-white/10 active:scale-95 disabled:opacity-30"
250
+ title="Attach Document"
251
+ >
252
+ <span className={`text-xl ${isUploading ? 'animate-spin' : ''}`}>{isUploading ? '⌛' : '📎'}</span>
253
+ </button>
254
+
255
  <input
256
  type="text"
257
  value={input}
258
  onChange={(e) => setInput(e.target.value)}
259
  onKeyDown={(e) => e.key === 'Enter' && handleSend()}
260
+ placeholder={isUploading ? "Uploading document..." : `Message ${selectedAgent.name}...`}
261
+ disabled={isUploading}
262
+ className="flex-1 bg-black/40 border border-white/10 rounded-2xl px-4 md:px-5 py-3 text-white text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all disabled:opacity-50"
263
  />
264
+
265
  <button
266
+ onClick={() => handleSend()}
267
+ disabled={!input.trim() || isLoading || isUploading}
268
+ className="w-10 h-10 md:w-12 md:h-12 rounded-2xl premium-gradient text-white flex items-center justify-center transition-all active:scale-95 disabled:opacity-30 shadow-lg shadow-purple-500/20"
269
  >
270
  <span className="text-xl">✈️</span>
271
  </button>