Skip to content

Commit 36b28e0

Browse files
Merge pull request #96 from DataScienceUIBK/demo
feat: add Agent Playground (RankifyAgent interface) and Model Arena (…
2 parents 6b6d3e7 + 053aa54 commit 36b28e0

4 files changed

Lines changed: 592 additions & 1 deletion

File tree

demo-web/src/app/agent/page.tsx

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"use client";
2+
3+
import { useState, useRef, useEffect } from "react";
4+
import { Send, Bot, User, Code2, Sparkles, ServerCrash, Loader2, ArrowRight } from "lucide-react";
5+
6+
interface AgentRecommendation {
7+
code_snippet: string | null;
8+
retriever: string | null;
9+
reranker: string | null;
10+
rag_method: string | null;
11+
}
12+
13+
interface ChatMsg {
14+
id: string;
15+
role: "user" | "assistant";
16+
content: string;
17+
recommendation?: AgentRecommendation | null;
18+
}
19+
20+
export default function AgentPage() {
21+
const [messages, setMessages] = useState<ChatMsg[]>([
22+
{
23+
id: "1", role: "assistant", content: "Hi! I am RankifyAgent. Tell me about your requirements (e.g. \"I need a fast pipeline with no GPU for medical data\") and I'll recommend the optimal retrieval, reranking, and generation setup for you."
24+
}
25+
]);
26+
const [input, setInput] = useState("");
27+
const [loading, setLoading] = useState(false);
28+
const [activeRec, setActiveRec] = useState<AgentRecommendation | null>(null);
29+
const bottomRef = useRef<HTMLDivElement>(null);
30+
const sessionId = useRef(Math.random().toString(36).slice(2) + Date.now().toString(36));
31+
32+
// Auto-scroll
33+
useEffect(() => {
34+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
35+
}, [messages]);
36+
37+
const handleSubmit = async (e: React.FormEvent) => {
38+
e.preventDefault();
39+
if (!input.trim() || loading) return;
40+
41+
const userMsg = input.trim();
42+
setInput("");
43+
const msgId = Math.random().toString(36).slice(2);
44+
45+
setMessages(prev => [...prev, { id: Date.now().toString(), role: "user", content: userMsg }]);
46+
setMessages(prev => [...prev, { id: msgId, role: "assistant", content: "" }]);
47+
setLoading(true);
48+
49+
try {
50+
const res = await fetch("/api/agent/chat", {
51+
method: "POST",
52+
headers: { "Content-Type": "application/json" },
53+
body: JSON.stringify({ message: userMsg, session_id: sessionId.current })
54+
});
55+
56+
if (!res.body) throw new Error("No response body");
57+
58+
const reader = res.body.getReader();
59+
const dec = new TextDecoder();
60+
let recData: AgentRecommendation | null = null;
61+
let buffer = "";
62+
63+
while (true) {
64+
const { done, value } = await reader.read();
65+
if (done) break;
66+
67+
buffer += dec.decode(value, { stream: true });
68+
const lines = buffer.split('\n');
69+
buffer = lines.pop() ?? '';
70+
71+
for (const line of lines) {
72+
if (!line.startsWith('data: ')) continue;
73+
const raw = line.slice(6).trim();
74+
if (!raw) continue;
75+
76+
try {
77+
const event = JSON.parse(raw);
78+
if (event.type === "token") {
79+
setMessages(prev => prev.map(m => m.id === msgId ? { ...m, content: m.content + event.content } : m));
80+
} else if (event.type === "recommendation") {
81+
recData = event.data;
82+
setMessages(prev => prev.map(m => m.id === msgId ? { ...m, recommendation: recData } : m));
83+
setActiveRec(recData); // auto-show the latest recommendation
84+
} else if (event.type === "error") {
85+
setMessages(prev => prev.map(m => m.id === msgId ? { ...m, content: m.content + "\n\n⚠️ Error: " + event.message } : m));
86+
}
87+
} catch (e) {
88+
// ignore parse errors for partial chunks
89+
}
90+
}
91+
}
92+
} catch (err: any) {
93+
setMessages(prev => prev.map(m => m.id === msgId ? { ...m, content: m.content + "\n\nConnection Error." } : m));
94+
} finally {
95+
setLoading(false);
96+
}
97+
};
98+
99+
return (
100+
<div className="flex h-[calc(100vh-56px)] overflow-hidden bg-white">
101+
{/* Chat Column */}
102+
<div className="flex-1 flex flex-col min-w-0 border-r border-slate-100">
103+
<div className="flex-1 overflow-y-auto p-4 md:p-8">
104+
<div className="max-w-3xl mx-auto flex flex-col gap-6">
105+
{messages.map(m => (
106+
<div key={m.id} className={`flex gap-4 ${m.role === "user" ? "flex-row-reverse" : "flex-row"}`}>
107+
<div className={`w-8 h-8 rounded-full flex shrink-0 items-center justify-center ${m.role === "user" ? "bg-slate-100 text-slate-600" : "bg-indigo-600 text-white shadow-md shadow-indigo-600/20"}`}>
108+
{m.role === "user" ? <User className="w-4 h-4" /> : <Sparkles className="w-4 h-4" />}
109+
</div>
110+
<div className={`flex flex-col gap-2 max-w-[80%] ${m.role === "user" ? "items-end" : "items-start"}`}>
111+
<div className={`p-4 rounded-2xl text-sm leading-relaxed ${m.role === "user" ? "bg-slate-100 text-slate-800 rounded-tr-sm" : "bg-white border border-slate-100 shadow-sm text-slate-700 rounded-tl-sm whitespace-pre-wrap"}`}>
112+
{m.content || <span className="animate-pulse text-slate-400">Thinking...</span>}
113+
</div>
114+
{/* Action button to view recommendation if it exists */}
115+
{m.recommendation && (
116+
<button
117+
onClick={() => setActiveRec(m.recommendation!)}
118+
className="group flex items-center gap-2 px-3 py-2 bg-indigo-50 border border-indigo-100 rounded-lg text-xs font-semibold text-indigo-700 hover:bg-indigo-100 transition-colors">
119+
<Code2 className="w-4 h-4 text-indigo-500 group-hover:scale-110 transition-transform" />
120+
View Configuration & Code
121+
<ArrowRight className="w-3 h-3 ml-1 opacity-50" />
122+
</button>
123+
)}
124+
</div>
125+
</div>
126+
))}
127+
<div ref={bottomRef} />
128+
</div>
129+
</div>
130+
131+
{/* Input Bar */}
132+
<div className="p-4 bg-white border-t border-slate-100">
133+
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto flex gap-2 relative">
134+
<input
135+
type="text"
136+
value={input}
137+
onChange={(e) => setInput(e.target.value)}
138+
placeholder="Describe your use case..."
139+
className="flex-1 h-12 px-4 rounded-xl border border-slate-200 bg-slate-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500/20 transition-all text-sm"
140+
disabled={loading}
141+
/>
142+
<button
143+
type="submit"
144+
disabled={!input.trim() || loading}
145+
className="h-12 w-12 flex items-center justify-center bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 disabled:opacity-50 transition-colors shadow-sm"
146+
>
147+
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : <Send className="w-5 h-5" />}
148+
</button>
149+
</form>
150+
</div>
151+
</div>
152+
153+
{/* Config & Code Panel (Right) */}
154+
<div className={`w-[400px] shrink-0 bg-slate-50 border-l border-slate-200 flex flex-col transition-all duration-300 ${activeRec ? "translate-x-0" : "translate-x-full hidden"}`}>
155+
<div className="p-4 border-b border-slate-200 bg-white flex items-center justify-between">
156+
<h2 className="font-bold text-slate-800 flex items-center gap-2">
157+
<Bot className="w-5 h-5 text-indigo-500" /> Recommended Setup
158+
</h2>
159+
<button onClick={() => setActiveRec(null)} className="text-slate-400 hover:text-slate-700 text-xs">Close</button>
160+
</div>
161+
162+
{activeRec && (
163+
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
164+
{/* Summary Badges */}
165+
<div className="flex flex-col gap-3">
166+
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Pipeline Components</h3>
167+
<div className="flex flex-col gap-2">
168+
<div className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-xl shadow-sm">
169+
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center text-emerald-600 text-sm font-bold shrink-0">1</div>
170+
<div className="min-w-0">
171+
<div className="text-[10px] uppercase font-bold text-slate-400">Retriever</div>
172+
<div className="text-sm font-semibold text-slate-800 truncate">{activeRec.retriever || "None"}</div>
173+
</div>
174+
</div>
175+
<div className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-xl shadow-sm">
176+
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center text-amber-600 text-sm font-bold shrink-0">2</div>
177+
<div className="min-w-0">
178+
<div className="text-[10px] uppercase font-bold text-slate-400">Reranker</div>
179+
<div className="text-sm font-semibold text-slate-800 truncate">{activeRec.reranker || "None"}</div>
180+
</div>
181+
</div>
182+
<div className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-xl shadow-sm">
183+
<div className="w-8 h-8 rounded-lg bg-violet-100 flex items-center justify-center text-violet-600 text-sm font-bold shrink-0">3</div>
184+
<div className="min-w-0">
185+
<div className="text-[10px] uppercase font-bold text-slate-400">Generator & Method</div>
186+
<div className="text-sm font-semibold text-slate-800 truncate">{activeRec.rag_method || "None"}</div>
187+
</div>
188+
</div>
189+
</div>
190+
</div>
191+
192+
{/* Code Snippet */}
193+
{activeRec.code_snippet && (
194+
<div className="flex flex-col gap-2">
195+
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center justify-between">
196+
<span>Python SDK Code</span>
197+
<button
198+
onClick={() => navigator.clipboard.writeText(activeRec.code_snippet!)}
199+
className="text-[10px] px-2 py-1 bg-indigo-100 text-indigo-700 rounded hover:bg-indigo-200 transition-colors">
200+
Copy
201+
</button>
202+
</h3>
203+
<div className="p-3 bg-[#0d1117] text-slate-300 font-mono text-[11px] rounded-xl overflow-x-auto border border-slate-800 shadow-inner">
204+
<pre><code>{activeRec.code_snippet}</code></pre>
205+
</div>
206+
</div>
207+
)}
208+
</div>
209+
)}
210+
</div>
211+
</div>
212+
);
213+
}

0 commit comments

Comments
 (0)