Spaces:
Running
Running
Commit ·
9b7c6ff
1
Parent(s): 230bf14
feat: implement ChatGPT style UI, streaming generation, math/code blocks, citations & multi-model fallback
Browse files- .env.example +1 -0
- config/settings.py +1 -0
- frontend-next/app/globals.css +43 -0
- frontend-next/app/layout.tsx +1 -0
- frontend-next/app/page.tsx +473 -802
- frontend-next/package-lock.json +309 -0
- frontend-next/package.json +2 -0
- src/api/main.py +43 -1
- src/rag/llm_client.py +120 -82
- src/rag/pipeline.py +70 -70
.env.example
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
GROQ_API_KEY=your_groq_api_key_here
|
|
|
|
|
|
| 1 |
GROQ_API_KEY=your_groq_api_key_here
|
| 2 |
+
HF_API_KEY=your_key_here
|
config/settings.py
CHANGED
|
@@ -80,6 +80,7 @@ TOP_K_RERANK = 5 # Keep top 5 after reranking
|
|
| 80 |
# LLM SETTINGS
|
| 81 |
# ------------------------------------------
|
| 82 |
GROQ_API_KEY = os.getenv('GROQ_API_KEY') # Loaded from .env
|
|
|
|
| 83 |
LLM_MODEL_NAME = 'llama-3.3-70b-versatile' # Groq model ID
|
| 84 |
LLM_TEMPERATURE = 0.1 # Low = More factual/consistent
|
| 85 |
LLM_MAX_TOKENS = 2048 # Max response tokens
|
|
|
|
| 80 |
# LLM SETTINGS
|
| 81 |
# ------------------------------------------
|
| 82 |
GROQ_API_KEY = os.getenv('GROQ_API_KEY') # Loaded from .env
|
| 83 |
+
HF_API_KEY = os.getenv('HF_API_KEY')
|
| 84 |
LLM_MODEL_NAME = 'llama-3.3-70b-versatile' # Groq model ID
|
| 85 |
LLM_TEMPERATURE = 0.1 # Low = More factual/consistent
|
| 86 |
LLM_MAX_TOKENS = 2048 # Max response tokens
|
frontend-next/app/globals.css
CHANGED
|
@@ -926,3 +926,46 @@ select {
|
|
| 926 |
color: var(--accent);
|
| 927 |
transform: translateY(-2px);
|
| 928 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
color: var(--accent);
|
| 927 |
transform: translateY(-2px);
|
| 928 |
}
|
| 929 |
+
|
| 930 |
+
/* -- UI Redesign -- */
|
| 931 |
+
.layout-wrapper { display: flex; height: 100vh; width: 100vw; overflow: hidden; }
|
| 932 |
+
.sidebar { width: 260px; background: rgba(5, 7, 10, 0.95); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; padding: 10px; z-index: 100; flex-shrink: 0; transition: transform 0.3s ease; }
|
| 933 |
+
@media (max-width: 768px) { .sidebar { position: fixed; height: 100vh; transform: translateX(-100%); } .sidebar.open { transform: translateX(0); } }
|
| 934 |
+
.sidebar-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 90; }
|
| 935 |
+
.new-chat-btn { display: flex; align-items: center; gap: 10px; width: 100%; padding: 12px; border-radius: 8px; background: rgba(255,255,255,0.05); color: #fff; font-weight: 500; font-size: 0.9rem; margin-bottom: 20px; transition: 0.2s; border: 1px solid rgba(255,255,255,0.1); }
|
| 936 |
+
.new-chat-btn:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.2); }
|
| 937 |
+
.history-item { padding: 10px; border-radius: 6px; font-size: 0.85rem; color: var(--text-muted); cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: 0.2s; }
|
| 938 |
+
.history-item:hover, .history-item.active { background: rgba(255,255,255,0.05); color: #fff; }
|
| 939 |
+
|
| 940 |
+
.main-chat-area { flex: 1; display: flex; flex-direction: column; position: relative; height: 100vh; }
|
| 941 |
+
.chat-container { flex: 1; overflow-y: auto; padding: 80px 20px 140px 20px; display: flex; flex-direction: column; gap: 24px; max-width: 900px; margin: 0 auto; width: 100%; }
|
| 942 |
+
|
| 943 |
+
.message-user { align-self: flex-end; background: rgba(255,255,255,0.1); color: #fff; padding: 12px 18px; border-radius: 20px; border-bottom-right-radius: 4px; max-width: 80%; }
|
| 944 |
+
.message-ai { align-self: flex-start; background: transparent; color: #e2e8f0; border-radius: 12px; width: 100%; }
|
| 945 |
+
|
| 946 |
+
.bottom-input-bar { position: fixed; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(3,5,8,0.9) 20%); padding: 20px; display: flex; justify-content: center; z-index: 50; pointer-events: none; }
|
| 947 |
+
@media (min-width: 769px) { .bottom-input-bar { left: 260px; } }
|
| 948 |
+
.bottom-input-bar-inner { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 8px; pointer-events: auto; }
|
| 949 |
+
|
| 950 |
+
.hamburger { display: none; }
|
| 951 |
+
@media (max-width: 768px) { .hamburger { display: flex; z-index: 200; position: fixed; top: 16px; left: 16px; width: 40px; height: 40px; border-radius: 8px; background: rgba(255,255,255,0.1); align-items: center; justify-content: center; backdrop-filter: blur(10px); } }
|
| 952 |
+
|
| 953 |
+
/* Code Block */
|
| 954 |
+
.code-header { display: flex; justify-content: space-between; align-items: center; background: #282c34; padding: 4px 12px; border-top-left-radius: 8px; border-top-right-radius: 8px; font-family: monospace; font-size: 0.8rem; color: #abb2bf; }
|
| 955 |
+
.code-wrapper { border-radius: 8px; overflow: hidden; margin: 12px 0; border: 1px solid rgba(255,255,255,0.1); }
|
| 956 |
+
.code-wrapper pre { margin: 0 !important; border-radius: 0 !important; }
|
| 957 |
+
|
| 958 |
+
/* Citations Pill */
|
| 959 |
+
.citation-badge { display: inline-flex; align-items: center; justify-content: center; background: #3b82f6; color: #fff; font-size: 11px; padding: 2px 6px; border-radius: 12px; font-weight: 600; cursor: pointer; text-decoration: none; vertical-align: super; line-height: 1; margin: 0 2px; }
|
| 960 |
+
.citation-badge:hover { background: #2563eb; }
|
| 961 |
+
|
| 962 |
+
/* Feedback row */
|
| 963 |
+
.feedback-row { display: flex; align-items: center; gap: 12px; margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.1); }
|
| 964 |
+
.star-btn { color: #4b5563; transition: 0.2s; cursor: pointer; }
|
| 965 |
+
.star-btn:hover, .star-btn.active { color: #f59e0b; }
|
| 966 |
+
.feedback-input { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); padding: 6px 12px; border-radius: 6px; font-size: 0.85rem; color: #fff; flex: 1; }
|
| 967 |
+
.feedback-submit { background: var(--accent); color: #000; padding: 6px 16px; border-radius: 6px; font-weight: 600; font-size: 0.85rem; }
|
| 968 |
+
|
| 969 |
+
.blinking-cursor { display: inline-block; width: 8px; height: 16px; background: #fff; animation: blink 1s step-end infinite; }
|
| 970 |
+
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
| 971 |
+
|
frontend-next/app/layout.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
import type { Metadata } from "next";
|
| 3 |
import { Inter } from "next/font/google";
|
| 4 |
import "./globals.css";
|
|
|
|
| 5 |
|
| 6 |
const inter = Inter({ subsets: ["latin"] });
|
| 7 |
|
|
|
|
| 2 |
import type { Metadata } from "next";
|
| 3 |
import { Inter } from "next/font/google";
|
| 4 |
import "./globals.css";
|
| 5 |
+
import 'katex/dist/katex.min.css';
|
| 6 |
|
| 7 |
const inter = Inter({ subsets: ["latin"] });
|
| 8 |
|
frontend-next/app/page.tsx
CHANGED
|
@@ -1,30 +1,19 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
import { useState, useRef, useEffect } from "react";
|
| 4 |
import { motion, AnimatePresence } from "framer-motion";
|
| 5 |
import { InlineMath, BlockMath } from 'react-katex';
|
| 6 |
-
import '
|
|
|
|
| 7 |
import {
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
Zap,
|
| 11 |
-
AlertCircle,
|
| 12 |
-
CheckCircle,
|
| 13 |
-
ExternalLink,
|
| 14 |
-
Sparkles,
|
| 15 |
-
Brain,
|
| 16 |
-
ArrowRight,
|
| 17 |
-
Layers,
|
| 18 |
-
Fingerprint,
|
| 19 |
-
Send,
|
| 20 |
-
Info,
|
| 21 |
-
X,
|
| 22 |
-
Server,
|
| 23 |
-
Activity,
|
| 24 |
-
Rocket,
|
| 25 |
} from "lucide-react";
|
| 26 |
|
| 27 |
-
//
|
|
|
|
|
|
|
|
|
|
| 28 |
interface Citation {
|
| 29 |
paper_id: string;
|
| 30 |
title: string;
|
|
@@ -33,840 +22,522 @@ interface Citation {
|
|
| 33 |
arxiv_url: string;
|
| 34 |
}
|
| 35 |
|
| 36 |
-
interface
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
chunks_used: number;
|
| 41 |
-
retrieval_time_ms: number;
|
| 42 |
-
generation_time_ms: number;
|
| 43 |
-
total_time_ms: number;
|
| 44 |
-
has_context: boolean;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
const CATEGORY_OPTIONS = [
|
| 57 |
-
{ value: "All", label: "All Topics" },
|
| 58 |
-
{ value: "cs.LG", label: "cs.LG", indexed: true },
|
| 59 |
-
{ value: "cs.AI", label: "cs.AI", indexed: true },
|
| 60 |
-
{ value: "stat.ML", label: "stat.ML", indexed: true },
|
| 61 |
-
{ value: "cs.CV", label: "cs.CV", indexed: false, disabled: true },
|
| 62 |
-
{ value: "cs.CL", label: "cs.CL", indexed: false, disabled: true },
|
| 63 |
-
{ value: "cs.RO", label: "cs.RO", indexed: false, disabled: true },
|
| 64 |
-
];
|
| 65 |
-
|
| 66 |
-
// ── Custom Dropdown Component ─────────────────────────────
|
| 67 |
-
function CustomSelect({
|
| 68 |
-
options,
|
| 69 |
-
value,
|
| 70 |
-
onChange,
|
| 71 |
-
width = '140px',
|
| 72 |
-
}: {
|
| 73 |
-
options: { value: string | number; label: string; disabled?: boolean; indexed?: boolean }[];
|
| 74 |
-
value: string | number;
|
| 75 |
-
onChange: (val: string | number) => void;
|
| 76 |
-
width?: string;
|
| 77 |
-
}) {
|
| 78 |
-
const [isOpen, setIsOpen] = useState(false);
|
| 79 |
-
const [placement, setPlacement] = useState<"top" | "bottom">("bottom");
|
| 80 |
-
const ref = useRef<HTMLDivElement>(null);
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
//
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
|
|
|
|
|
|
|
|
|
| 105 |
return (
|
| 106 |
-
<div
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
{isOpen && (
|
| 117 |
-
<motion.div
|
| 118 |
-
initial={{ opacity: 0, y: placement === 'top' ? 10 : -10, scale: 0.95 }}
|
| 119 |
-
animate={{ opacity: 1, y: 0, scale: 1 }}
|
| 120 |
-
exit={{ opacity: 0, y: placement === 'top' ? 5 : -5, scale: 0.95 }}
|
| 121 |
-
transition={{ duration: 0.15, ease: 'easeOut' }}
|
| 122 |
-
className="custom-dropdown-menu"
|
| 123 |
-
style={{
|
| 124 |
-
top: placement === 'bottom' ? 'calc(100% + 8px)' : 'auto',
|
| 125 |
-
bottom: placement === 'top' ? 'calc(100% + 8px)' : 'auto'
|
| 126 |
-
}}
|
| 127 |
-
>
|
| 128 |
-
{options.map((opt) => (
|
| 129 |
-
<button
|
| 130 |
-
key={opt.value}
|
| 131 |
-
onClick={() => {
|
| 132 |
-
if (opt.disabled) return;
|
| 133 |
-
onChange(opt.value);
|
| 134 |
-
setIsOpen(false);
|
| 135 |
-
}}
|
| 136 |
-
disabled={opt.disabled}
|
| 137 |
-
className={`custom-dropdown-item ${value === opt.value ? 'active' : ''}`}
|
| 138 |
-
style={opt.disabled ? { opacity: 0.4, cursor: 'not-allowed' } : {}}
|
| 139 |
-
>
|
| 140 |
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
|
| 141 |
-
<span>{opt.label}</span>
|
| 142 |
-
{opt.indexed !== undefined && (
|
| 143 |
-
<div style={{
|
| 144 |
-
fontSize: "0.65em",
|
| 145 |
-
fontWeight: 600,
|
| 146 |
-
padding: "2px 6px",
|
| 147 |
-
borderRadius: "12px",
|
| 148 |
-
backgroundColor: opt.indexed ? "rgba(16, 185, 129, 0.15)" : "rgba(156, 163, 175, 0.1)",
|
| 149 |
-
color: opt.indexed ? "var(--success)" : "var(--text-muted)",
|
| 150 |
-
marginLeft: "8px"
|
| 151 |
-
}}>
|
| 152 |
-
{opt.indexed ? "INDEXED" : "UNAVAILABLE"}
|
| 153 |
-
</div>
|
| 154 |
-
)}
|
| 155 |
-
</div>
|
| 156 |
-
</button>
|
| 157 |
-
))}
|
| 158 |
-
</motion.div>
|
| 159 |
-
)}
|
| 160 |
-
</AnimatePresence>
|
| 161 |
</div>
|
| 162 |
);
|
| 163 |
-
}
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
<span key={i}>
|
| 175 |
-
{inlineSplit.map((inlinePart, j) => {
|
| 176 |
-
if (inlinePart.startsWith("$") && inlinePart.endsWith("$")) {
|
| 177 |
-
const math = inlinePart.slice(1, -1);
|
| 178 |
-
return <InlineMath key={j} math={math} />;
|
| 179 |
-
}
|
| 180 |
-
return (
|
| 181 |
-
<span key={j} style={{ whiteSpace: "pre-wrap" }}>
|
| 182 |
-
{inlinePart}
|
| 183 |
-
</span>
|
| 184 |
-
);
|
| 185 |
-
})}
|
| 186 |
-
</span>
|
| 187 |
-
);
|
| 188 |
-
});
|
| 189 |
-
}
|
| 190 |
|
| 191 |
-
/
|
| 192 |
-
export default function Home() {
|
| 193 |
-
const [question, setQuestion] = useState("");
|
| 194 |
-
const [result, setResult] = useState<QueryResult | null>(null);
|
| 195 |
-
const [loading, setLoading] = useState(false);
|
| 196 |
-
const [error, setError] = useState("");
|
| 197 |
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
const [topK, setTopK] = useState(5);
|
| 200 |
const [category, setCategory] = useState("All");
|
| 201 |
-
const [yearFilter, setYearFilter] = useState(false);
|
| 202 |
-
const [yearFrom, setYearFrom] = useState(2024);
|
| 203 |
-
const [showSettings, setShowSettings] = useState(false);
|
| 204 |
-
|
| 205 |
-
// System state
|
| 206 |
-
const [apiStatus, setApiStatus] = useState<
|
| 207 |
-
"unknown" | "online" | "offline"
|
| 208 |
-
>("unknown");
|
| 209 |
-
const [showInfo, setShowInfo] = useState(false);
|
| 210 |
-
const [showStatusDetails, setShowStatusDetails] = useState(false);
|
| 211 |
|
|
|
|
| 212 |
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 213 |
|
|
|
|
| 214 |
useEffect(() => {
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
}
|
| 219 |
-
}, [
|
| 220 |
|
|
|
|
| 221 |
useEffect(() => {
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
setApiStatus(r.ok ? "online" : "offline");
|
| 231 |
-
} catch {
|
| 232 |
-
setApiStatus("offline");
|
| 233 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
};
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
};
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
-
|
|
|
|
| 251 |
method: "POST",
|
| 252 |
headers: { "Content-Type": "application/json" },
|
| 253 |
-
body: JSON.stringify(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
});
|
| 255 |
|
| 256 |
-
if (!
|
| 257 |
-
|
| 258 |
-
const
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
} finally {
|
| 267 |
-
|
| 268 |
}
|
| 269 |
};
|
| 270 |
|
| 271 |
-
const resetApp = () => {
|
| 272 |
-
setQuestion("");
|
| 273 |
-
setResult(null);
|
| 274 |
-
setError("");
|
| 275 |
-
setLoading(false);
|
| 276 |
-
};
|
| 277 |
-
|
| 278 |
-
const hasSearched = result || loading || error;
|
| 279 |
-
|
| 280 |
return (
|
| 281 |
-
<>
|
| 282 |
-
|
| 283 |
-
<div className="
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 293 |
-
onClick={(e) => e.stopPropagation()}
|
| 294 |
-
className="cyber-panel info-modal"
|
| 295 |
-
>
|
| 296 |
-
<button className="modal-close" onClick={() => setShowInfo(false)}>
|
| 297 |
-
<X size={18} />
|
| 298 |
-
</button>
|
| 299 |
-
<h2>ResearchPilot Console</h2>
|
| 300 |
-
<div style={{ display: "flex", alignItems: "center", gap: "12px", marginTop: "16px" }}>
|
| 301 |
-
<div style={{ background: "rgba(138, 43, 226, 0.15)", border: "1px solid rgba(138, 43, 226, 0.4)", padding: "6px 14px", borderRadius: "99px", fontSize: "0.75rem", color: "var(--accent-2)", fontWeight: 700, letterSpacing: "0.05em", textTransform: "uppercase" }}>
|
| 302 |
-
Lead Architect
|
| 303 |
-
</div>
|
| 304 |
-
<span style={{ fontFamily: "'Dancing Script', cursive", fontSize: "1.8rem", fontWeight: 700, background: "linear-gradient(135deg, #fff 20%, var(--accent-2) 100%)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", letterSpacing: "0.05em", transform: "translateY(-2px)" }}>Subhadip Hensh</span>
|
| 305 |
-
</div>
|
| 306 |
-
|
| 307 |
-
<hr />
|
| 308 |
-
|
| 309 |
-
<h3><Server size={18} /> System Overview</h3>
|
| 310 |
-
<p style={{ fontSize: "1rem", lineHeight: 1.7, marginBottom: "16px" }}>ResearchPilot is a high-performance RAG (Retrieval-Augmented Generation) engine tailored for Machine Learning literature. It features hybrid sparse-dense searching, advanced cross-encoder reranking, and GPU-driven vector indexing via Qdrant.</p>
|
| 311 |
-
|
| 312 |
-
<h3><Activity size={18} /> Current Operational Capacity</h3>
|
| 313 |
-
<ul>
|
| 314 |
-
<li><strong>Current Index</strong> Synthesizing 51,019 dense embeddings isolated from ~700 major AI & ML papers.</li>
|
| 315 |
-
<li><strong>Data Categories</strong> Fully indexed on core Machine Learning (cs.LG) and AI (cs.AI).</li>
|
| 316 |
-
</ul>
|
| 317 |
-
|
| 318 |
-
<h3><Layers size={18} /> Core Technology Stack</h3>
|
| 319 |
-
<ul>
|
| 320 |
-
<li><strong>Frontend Application</strong> Next.js 16 (App Router), React, Framer Motion, Vanilla CSS (Glassmorphism).</li>
|
| 321 |
-
<li><strong>Backend Environment</strong> Python, FastAPI, Uvicorn, Pydantic.</li>
|
| 322 |
-
<li><strong>Vector Database Engine</strong> Qdrant (GPU Accelerated Dense Vectors).</li>
|
| 323 |
-
<li><strong>RAG Processing Pipeline</strong> SentenceTransformers (BGE-base), BM25 Sparse Search, Cross-Encoder Reranking, Groq LLM (LLaMA 3.3).</li>
|
| 324 |
-
<li><strong>Mathematics Engine</strong> KaTeX & React-KaTeX for fully dynamic native LaTeX equations.</li>
|
| 325 |
-
</ul>
|
| 326 |
-
|
| 327 |
-
<h3><Rocket size={18} /> Phase 2: In-Progress Architecture</h3>
|
| 328 |
-
<ul>
|
| 329 |
-
<li><strong>Massive Data Expansion</strong> Scaling dataset soon to 10,000+ — 20,000+ ML papers spanning NLP, Computer Vision, and Robotics.</li>
|
| 330 |
-
<li><strong>Distributed Hardware Execution</strong> Scaling ingestion logic to cloud-based GPU clusters for extreme speed.</li>
|
| 331 |
-
<li><strong>Multi-modal Analysis</strong> Soon integrating visual graph and chart processing abilities into the synthesis engine.</li>
|
| 332 |
-
</ul>
|
| 333 |
-
</motion.div>
|
| 334 |
</div>
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
</div>
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
>
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
textAlign: "center",
|
| 410 |
-
display: "block",
|
| 411 |
-
width: "100%",
|
| 412 |
-
cursor: "pointer",
|
| 413 |
-
transition: "0.2s"
|
| 414 |
-
}}
|
| 415 |
-
onMouseEnter={(e) => {
|
| 416 |
-
e.currentTarget.style.background = "rgba(0, 240, 255, 0.2)";
|
| 417 |
-
}}
|
| 418 |
-
onMouseLeave={(e) => {
|
| 419 |
-
e.currentTarget.style.background = "rgba(0, 240, 255, 0.1)";
|
| 420 |
-
}}
|
| 421 |
-
>
|
| 422 |
-
Re-verify Connection
|
| 423 |
-
</button>
|
| 424 |
-
</motion.div>
|
| 425 |
)}
|
| 426 |
-
</
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
style={{ width: "100%" }}
|
| 459 |
-
>
|
| 460 |
-
<h1 className="hero-title">
|
| 461 |
-
Decipher the latest
|
| 462 |
-
<br />
|
| 463 |
-
<span className="text-gradient-2">
|
| 464 |
-
ML Research
|
| 465 |
-
</span>
|
| 466 |
-
</h1>
|
| 467 |
-
<p className="hero-subtitle">
|
| 468 |
-
Neural hybrid search across ArXiv. <br />
|
| 469 |
-
Cross-encoder reranked. LLM synthesized.
|
| 470 |
-
</p>
|
| 471 |
-
<div style={{ height: "64px" }} />
|
| 472 |
-
</motion.div>
|
| 473 |
-
)}
|
| 474 |
-
</AnimatePresence>
|
| 475 |
-
|
| 476 |
-
{/* ── ChatGPT Style Search Box ── */}
|
| 477 |
-
<motion.div
|
| 478 |
-
layout
|
| 479 |
-
style={{
|
| 480 |
-
width: "100%",
|
| 481 |
-
margin: "0 auto",
|
| 482 |
-
zIndex: 20,
|
| 483 |
-
}}
|
| 484 |
-
>
|
| 485 |
<div className="chat-input-wrapper">
|
| 486 |
-
<
|
|
|
|
|
|
|
|
|
|
| 487 |
ref={textareaRef}
|
| 488 |
-
value={
|
| 489 |
-
onChange={(e) =>
|
| 490 |
-
placeholder="Message ResearchPilot..."
|
| 491 |
-
rows={1}
|
| 492 |
-
className="chat-input"
|
| 493 |
-
style={{
|
| 494 |
-
minHeight: hasSearched ? "20px" : "28px",
|
| 495 |
-
maxHeight: "120px",
|
| 496 |
-
overflowY: "auto",
|
| 497 |
-
}}
|
| 498 |
onKeyDown={(e) => {
|
| 499 |
-
if (e.key ===
|
| 500 |
e.preventDefault();
|
| 501 |
-
|
| 502 |
}
|
| 503 |
}}
|
|
|
|
|
|
|
|
|
|
| 504 |
/>
|
| 505 |
-
<button
|
| 506 |
-
|
| 507 |
-
disabled={loading || !question.trim()}
|
| 508 |
-
className="send-btn"
|
| 509 |
-
>
|
| 510 |
-
{loading ? (
|
| 511 |
-
<div className="spinner-micro" />
|
| 512 |
-
) : (
|
| 513 |
-
<Send
|
| 514 |
-
size={18}
|
| 515 |
-
strokeWidth={2.5}
|
| 516 |
-
style={{ marginLeft: "-2px" }}
|
| 517 |
-
/>
|
| 518 |
-
)}
|
| 519 |
</button>
|
| 520 |
</div>
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
color: "var(--text-muted)",
|
| 528 |
-
fontSize: "0.8rem",
|
| 529 |
-
fontWeight: 600,
|
| 530 |
-
padding: "4px",
|
| 531 |
-
cursor: "pointer",
|
| 532 |
-
}}
|
| 533 |
-
>
|
| 534 |
-
<Layers size={14} />
|
| 535 |
-
CONFIGURE {showSettings ? "▲" : "▼"}
|
| 536 |
-
</button>
|
| 537 |
-
|
| 538 |
-
<div className="controls-group">
|
| 539 |
-
<AnimatePresence>
|
| 540 |
-
{showSettings && (
|
| 541 |
-
<motion.div
|
| 542 |
-
initial={{ opacity: 0, height: 0 }}
|
| 543 |
-
animate={{
|
| 544 |
-
opacity: 1,
|
| 545 |
-
height: "auto",
|
| 546 |
-
}}
|
| 547 |
-
exit={{ opacity: 0, height: 0 }}
|
| 548 |
-
style={{
|
| 549 |
-
overflow: "visible",
|
| 550 |
-
display: "flex",
|
| 551 |
-
gap: "12px",
|
| 552 |
-
flexWrap: "wrap",
|
| 553 |
-
alignItems: "center",
|
| 554 |
-
}}
|
| 555 |
-
>
|
| 556 |
-
<CustomSelect
|
| 557 |
-
value={topK}
|
| 558 |
-
onChange={(val) => setTopK(Number(val))}
|
| 559 |
-
options={[
|
| 560 |
-
{ value: 3, label: 'Top 3 Results' },
|
| 561 |
-
{ value: 5, label: 'Top 5 Results' },
|
| 562 |
-
{ value: 10, label: 'Top 10 Results' },
|
| 563 |
-
]}
|
| 564 |
-
/>
|
| 565 |
-
<CustomSelect
|
| 566 |
-
value={category}
|
| 567 |
-
onChange={(val) => setCategory(String(val))}
|
| 568 |
-
options={CATEGORY_OPTIONS}
|
| 569 |
-
width="160px"
|
| 570 |
-
/>
|
| 571 |
-
<button
|
| 572 |
-
onClick={() =>
|
| 573 |
-
setYearFilter(!yearFilter)
|
| 574 |
-
}
|
| 575 |
-
className={`cyber-btn-outline ${yearFilter ? "active" : ""}`}
|
| 576 |
-
>
|
| 577 |
-
YEAR FILTER{" "}
|
| 578 |
-
{yearFilter ? "ON" : "OFF"}
|
| 579 |
-
</button>
|
| 580 |
-
{yearFilter && (
|
| 581 |
-
<div className="year-stepper">
|
| 582 |
-
<button onClick={() => setYearFrom(y => Math.max(2000, y - 1))} className="stepper-btn">-</button>
|
| 583 |
-
<input
|
| 584 |
-
type="number"
|
| 585 |
-
value={yearFrom}
|
| 586 |
-
readOnly
|
| 587 |
-
className="stepper-input"
|
| 588 |
-
/>
|
| 589 |
-
<button onClick={() => setYearFrom(y => Math.min(2026, y + 1))} className="stepper-btn">+</button>
|
| 590 |
-
</div>
|
| 591 |
-
)}
|
| 592 |
-
</motion.div>
|
| 593 |
-
)}
|
| 594 |
-
</AnimatePresence>
|
| 595 |
-
</div>
|
| 596 |
-
</div>
|
| 597 |
-
</motion.div>
|
| 598 |
-
|
| 599 |
-
{/* ── Example Queries ── */}
|
| 600 |
-
<AnimatePresence>
|
| 601 |
-
{!hasSearched && (
|
| 602 |
-
<motion.div
|
| 603 |
-
layout
|
| 604 |
-
initial={{ opacity: 0 }}
|
| 605 |
-
animate={{ opacity: 1 }}
|
| 606 |
-
exit={{ opacity: 0, filter: "blur(5px)" }}
|
| 607 |
-
className="example-chips"
|
| 608 |
-
>
|
| 609 |
-
{EXAMPLE_QUERIES.map((q, i) => (
|
| 610 |
-
<motion.button
|
| 611 |
-
key={q}
|
| 612 |
-
initial={{ opacity: 0, y: 10 }}
|
| 613 |
-
animate={{ opacity: 1, y: 0 }}
|
| 614 |
-
transition={{ delay: 0.1 * i + 0.3 }}
|
| 615 |
-
onClick={() => {
|
| 616 |
-
setQuestion(q);
|
| 617 |
-
setTimeout(
|
| 618 |
-
() => handleSearch(),
|
| 619 |
-
50,
|
| 620 |
-
);
|
| 621 |
-
}}
|
| 622 |
-
className="chip"
|
| 623 |
-
>
|
| 624 |
-
{q}
|
| 625 |
-
</motion.button>
|
| 626 |
-
))}
|
| 627 |
-
</motion.div>
|
| 628 |
-
)}
|
| 629 |
-
</AnimatePresence>
|
| 630 |
-
</motion.div>
|
| 631 |
-
|
| 632 |
-
{/* ── Error State ── */}
|
| 633 |
-
<AnimatePresence>
|
| 634 |
-
{error && (
|
| 635 |
-
<motion.div
|
| 636 |
-
initial={{ opacity: 0, y: 10 }}
|
| 637 |
-
animate={{ opacity: 1, y: 0 }}
|
| 638 |
-
className="cyber-panel"
|
| 639 |
-
style={{
|
| 640 |
-
width: "100%",
|
| 641 |
-
padding: "24px",
|
| 642 |
-
borderColor: "var(--danger)",
|
| 643 |
-
marginTop: "24px",
|
| 644 |
-
}}
|
| 645 |
-
>
|
| 646 |
-
<div
|
| 647 |
-
style={{
|
| 648 |
-
display: "flex",
|
| 649 |
-
gap: "16px",
|
| 650 |
-
alignItems: "center",
|
| 651 |
-
}}
|
| 652 |
-
>
|
| 653 |
-
<AlertCircle size={32} color="var(--danger)" />
|
| 654 |
-
<div>
|
| 655 |
-
<h3
|
| 656 |
-
style={{
|
| 657 |
-
color: "var(--danger)",
|
| 658 |
-
fontSize: "1.2rem",
|
| 659 |
-
marginBottom: "4px",
|
| 660 |
-
}}
|
| 661 |
-
>
|
| 662 |
-
Critical Exception
|
| 663 |
-
</h3>
|
| 664 |
-
<p style={{ color: "var(--text-muted)" }}>
|
| 665 |
-
{error}
|
| 666 |
-
</p>
|
| 667 |
-
</div>
|
| 668 |
-
</div>
|
| 669 |
-
</motion.div>
|
| 670 |
-
)}
|
| 671 |
-
</AnimatePresence>
|
| 672 |
-
|
| 673 |
-
{/* ── Loading View ── */}
|
| 674 |
-
<AnimatePresence>
|
| 675 |
-
{loading && (
|
| 676 |
-
<motion.div
|
| 677 |
-
initial={{ opacity: 0 }}
|
| 678 |
-
animate={{ opacity: 1 }}
|
| 679 |
-
exit={{ opacity: 0 }}
|
| 680 |
-
className="loader-view"
|
| 681 |
-
style={{ width: "100%" }}
|
| 682 |
-
>
|
| 683 |
-
<div className="ring-spinner">
|
| 684 |
-
<div className="ring ring-1" />
|
| 685 |
-
<div className="ring ring-2" />
|
| 686 |
-
<Brain
|
| 687 |
-
size={22}
|
| 688 |
-
style={{
|
| 689 |
-
position: "absolute",
|
| 690 |
-
top: "24px",
|
| 691 |
-
left: "24px",
|
| 692 |
-
color: "var(--text-main)",
|
| 693 |
-
}}
|
| 694 |
-
/>
|
| 695 |
-
</div>
|
| 696 |
-
<div>
|
| 697 |
-
<h2
|
| 698 |
-
style={{
|
| 699 |
-
fontSize: "1.4rem",
|
| 700 |
-
fontWeight: 600,
|
| 701 |
-
color: "#fff",
|
| 702 |
-
marginBottom: "8px",
|
| 703 |
-
}}
|
| 704 |
-
>
|
| 705 |
-
Synthesizing Knowledge
|
| 706 |
-
</h2>
|
| 707 |
-
<p
|
| 708 |
-
style={{
|
| 709 |
-
color: "var(--text-muted)",
|
| 710 |
-
fontSize: "0.95rem",
|
| 711 |
-
}}
|
| 712 |
-
>
|
| 713 |
-
Running Vector Search & LLM Inference
|
| 714 |
-
</p>
|
| 715 |
-
</div>
|
| 716 |
-
</motion.div>
|
| 717 |
-
)}
|
| 718 |
-
</AnimatePresence>
|
| 719 |
-
|
| 720 |
-
{/* ── Results Output ── */}
|
| 721 |
-
{result && !loading && (
|
| 722 |
-
<motion.div
|
| 723 |
-
initial={{ opacity: 0, y: 20 }}
|
| 724 |
-
animate={{ opacity: 1, y: 0 }}
|
| 725 |
-
transition={{ staggerChildren: 0.1 }}
|
| 726 |
-
className="results-area"
|
| 727 |
-
>
|
| 728 |
-
<motion.div
|
| 729 |
-
className="cyber-panel answer-box"
|
| 730 |
-
initial={{ opacity: 0, y: 20 }}
|
| 731 |
-
animate={{ opacity: 1, y: 0 }}
|
| 732 |
-
>
|
| 733 |
-
<div className="answer-header">
|
| 734 |
-
<div className="answer-label">
|
| 735 |
-
<Sparkles size={20} /> AI Synthesis
|
| 736 |
-
</div>
|
| 737 |
-
{result.has_context ? (
|
| 738 |
-
<div className="badge-grounded">
|
| 739 |
-
<CheckCircle size={14} /> Grounded
|
| 740 |
-
Sources
|
| 741 |
-
</div>
|
| 742 |
-
) : (
|
| 743 |
-
<div
|
| 744 |
-
className="badge-grounded"
|
| 745 |
-
style={{
|
| 746 |
-
color: "var(--danger)",
|
| 747 |
-
borderColor:
|
| 748 |
-
"rgba(239, 68, 68, 0.3)",
|
| 749 |
-
background:
|
| 750 |
-
"rgba(239, 68, 68, 0.1)",
|
| 751 |
-
}}
|
| 752 |
-
>
|
| 753 |
-
<AlertCircle size={14} /> Hallucination
|
| 754 |
-
Risk
|
| 755 |
-
</div>
|
| 756 |
-
)}
|
| 757 |
-
</div>
|
| 758 |
-
<div className="answer-text">{renderLaTeX(result.answer)}</div>
|
| 759 |
-
</motion.div>
|
| 760 |
-
|
| 761 |
-
<motion.div
|
| 762 |
-
className="stats-grid"
|
| 763 |
-
initial={{ opacity: 0, y: 20 }}
|
| 764 |
-
animate={{ opacity: 1, y: 0 }}
|
| 765 |
-
>
|
| 766 |
-
{[
|
| 767 |
-
{
|
| 768 |
-
l: "Execution Time",
|
| 769 |
-
v: `${(result.total_time_ms / 1000).toFixed(1)}s`,
|
| 770 |
-
i: Clock,
|
| 771 |
-
c: "var(--accent)",
|
| 772 |
-
},
|
| 773 |
-
{
|
| 774 |
-
l: "Vector Data",
|
| 775 |
-
v: `${(result.retrieval_time_ms / 1000).toFixed(1)}s`,
|
| 776 |
-
i: ArrowRight,
|
| 777 |
-
c: "#fff",
|
| 778 |
-
},
|
| 779 |
-
{
|
| 780 |
-
l: "LLM Generation",
|
| 781 |
-
v: `${(result.generation_time_ms / 1000).toFixed(1)}s`,
|
| 782 |
-
i: Zap,
|
| 783 |
-
c: "var(--accent-2)",
|
| 784 |
-
},
|
| 785 |
-
{
|
| 786 |
-
l: "Paper Chunks",
|
| 787 |
-
v: result.chunks_used,
|
| 788 |
-
i: Fingerprint,
|
| 789 |
-
c: "var(--success)",
|
| 790 |
-
},
|
| 791 |
-
].map((s, i) => (
|
| 792 |
-
<div key={i} className="cyber-panel stat-card">
|
| 793 |
-
<div
|
| 794 |
-
className="stat-header"
|
| 795 |
-
style={{ width: "100%" }}
|
| 796 |
-
>
|
| 797 |
-
<span className="stat-label">
|
| 798 |
-
{s.l}
|
| 799 |
-
</span>
|
| 800 |
-
<s.i
|
| 801 |
-
size={16}
|
| 802 |
-
color={s.c}
|
| 803 |
-
style={{ opacity: 0.8 }}
|
| 804 |
-
/>
|
| 805 |
-
</div>
|
| 806 |
-
<div
|
| 807 |
-
className="stat-value"
|
| 808 |
-
style={{
|
| 809 |
-
width: "100%",
|
| 810 |
-
textAlign: "left",
|
| 811 |
-
color: s.c,
|
| 812 |
-
}}
|
| 813 |
-
>
|
| 814 |
-
{s.v}
|
| 815 |
-
</div>
|
| 816 |
-
</div>
|
| 817 |
-
))}
|
| 818 |
-
</motion.div>
|
| 819 |
-
|
| 820 |
-
{result.citations.length > 0 && (
|
| 821 |
-
<motion.div
|
| 822 |
-
initial={{ opacity: 0, y: 20 }}
|
| 823 |
-
animate={{ opacity: 1, y: 0 }}
|
| 824 |
-
>
|
| 825 |
-
<div className="section-title">
|
| 826 |
-
<BookOpen
|
| 827 |
-
size={18}
|
| 828 |
-
color="var(--accent-2)"
|
| 829 |
-
/>
|
| 830 |
-
Extracted Literature
|
| 831 |
-
</div>
|
| 832 |
-
<div className="citations-grid">
|
| 833 |
-
{result.citations.map((cite, i) => (
|
| 834 |
-
<a
|
| 835 |
-
href={cite.arxiv_url}
|
| 836 |
-
target="_blank"
|
| 837 |
-
rel="noopener noreferrer"
|
| 838 |
-
key={i}
|
| 839 |
-
className="cyber-panel citation-card"
|
| 840 |
-
>
|
| 841 |
-
<div className="citation-open">
|
| 842 |
-
<ExternalLink size={16} />
|
| 843 |
-
</div>
|
| 844 |
-
<div className="citation-meta">
|
| 845 |
-
<span className="citation-id">
|
| 846 |
-
{cite.paper_id}
|
| 847 |
-
</span>
|
| 848 |
-
<span className="citation-date">
|
| 849 |
-
{cite.published_date}
|
| 850 |
-
</span>
|
| 851 |
-
</div>
|
| 852 |
-
<h4 className="citation-title">
|
| 853 |
-
{cite.title}
|
| 854 |
-
</h4>
|
| 855 |
-
<div className="citation-authors">
|
| 856 |
-
{cite.authors
|
| 857 |
-
.slice(0, 3)
|
| 858 |
-
.join(", ")}
|
| 859 |
-
{cite.authors.length > 3 &&
|
| 860 |
-
` +${cite.authors.length - 3} more`}
|
| 861 |
-
</div>
|
| 862 |
-
</a>
|
| 863 |
-
))}
|
| 864 |
-
</div>
|
| 865 |
-
</motion.div>
|
| 866 |
-
)}
|
| 867 |
-
</motion.div>
|
| 868 |
-
)}
|
| 869 |
-
</main>
|
| 870 |
-
</>
|
| 871 |
);
|
| 872 |
}
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
|
| 3 |
import { useState, useRef, useEffect } from "react";
|
| 4 |
import { motion, AnimatePresence } from "framer-motion";
|
| 5 |
import { InlineMath, BlockMath } from 'react-katex';
|
| 6 |
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
| 7 |
+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
| 8 |
import {
|
| 9 |
+
Brain, Search, PanelLeftClose, PanelLeft, Plus,
|
| 10 |
+
Send, Settings2, Trash2, Copy, Check, Star, ThumbsUp, ThumbsDown
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
} from "lucide-react";
|
| 12 |
|
| 13 |
+
// Config
|
| 14 |
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
| 15 |
+
|
| 16 |
+
// --- Types ---
|
| 17 |
interface Citation {
|
| 18 |
paper_id: string;
|
| 19 |
title: string;
|
|
|
|
| 22 |
arxiv_url: string;
|
| 23 |
}
|
| 24 |
|
| 25 |
+
interface MessageTiming {
|
| 26 |
+
retrieval_time_ms?: number;
|
| 27 |
+
generation_time_ms?: number;
|
| 28 |
+
total_time_ms?: number;
|
| 29 |
+
chunks_used?: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
+
interface ChatMessage {
|
| 33 |
+
id: string;
|
| 34 |
+
role: "user" | "assistant";
|
| 35 |
+
content: string;
|
| 36 |
+
citations?: Citation[];
|
| 37 |
+
timing?: MessageTiming;
|
| 38 |
+
model_used?: string;
|
| 39 |
+
timestamp: number;
|
| 40 |
+
}
|
| 41 |
|
| 42 |
+
interface ChatSession {
|
| 43 |
+
id: string;
|
| 44 |
+
title: string;
|
| 45 |
+
messages: ChatMessage[];
|
| 46 |
+
timestamp: number;
|
| 47 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
// --- Message Renderer Component ---
|
| 50 |
+
const CodeBlock = ({ language, code }: { language: string, code: string }) => {
|
| 51 |
+
const [copied, setCopied] = useState(false);
|
| 52 |
+
const handleCopy = () => {
|
| 53 |
+
navigator.clipboard.writeText(code);
|
| 54 |
+
setCopied(true);
|
| 55 |
+
setTimeout(() => setCopied(false), 2000);
|
| 56 |
+
};
|
| 57 |
+
return (
|
| 58 |
+
<div className="code-wrapper">
|
| 59 |
+
<div className="code-header">
|
| 60 |
+
<span>{language || 'text'}</span>
|
| 61 |
+
<button onClick={handleCopy} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
| 62 |
+
{copied ? <><Check size={14} /> Copied!</> : <><Copy size={14} /> Copy</>}
|
| 63 |
+
</button>
|
| 64 |
+
</div>
|
| 65 |
+
<SyntaxHighlighter language={language || 'text'} style={oneDark} customStyle={{ margin: 0, borderTopLeftRadius: 0, borderTopRightRadius: 0 }}>
|
| 66 |
+
{code}
|
| 67 |
+
</SyntaxHighlighter>
|
| 68 |
+
</div>
|
| 69 |
+
);
|
| 70 |
+
};
|
| 71 |
|
| 72 |
+
const CitationBadge = ({ id }: { id: string }) => {
|
| 73 |
+
return (
|
| 74 |
+
<a
|
| 75 |
+
href={`https://arxiv.org/abs/${id}`}
|
| 76 |
+
target="_blank"
|
| 77 |
+
rel="noopener noreferrer"
|
| 78 |
+
className="citation-badge"
|
| 79 |
+
title={`View paper ${id} on ArXiv`}
|
| 80 |
+
>
|
| 81 |
+
{id}
|
| 82 |
+
</a>
|
| 83 |
+
);
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
// Safe rendering to avoid KaTeX crashing
|
| 87 |
+
const SafeMathBlock = ({ math }: { math: string }) => {
|
| 88 |
+
try { return <BlockMath math={math} />; } catch (e) { return <pre>{`$$${math}$$`}</pre>; }
|
| 89 |
+
};
|
| 90 |
+
const SafeMathInline = ({ math }: { math: string }) => {
|
| 91 |
+
try { return <InlineMath math={math} />; } catch (e) { return <span>{`$${math}$`}</span>; }
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
const renderTextWithMathAndCitations = (text: string) => {
|
| 95 |
+
// 1. Block Math
|
| 96 |
+
const blockSplit = text.split(/(\$\$[\s\S]+?\$\$)/g);
|
| 97 |
+
return blockSplit.map((bPart, i) => {
|
| 98 |
+
if (bPart.startsWith("$$") && bPart.endsWith("$$")) {
|
| 99 |
+
return <SafeMathBlock key={i} math={bPart.slice(2, -2)} />;
|
| 100 |
}
|
| 101 |
+
|
| 102 |
+
// 2. Inline Math
|
| 103 |
+
const inlineSplit = bPart.split(/(\$[^\$\n]+?\$)/g);
|
| 104 |
+
return inlineSplit.map((iPart, j) => {
|
| 105 |
+
if (iPart.startsWith("$") && iPart.endsWith("$")) {
|
| 106 |
+
return <SafeMathInline key={`${i}-${j}`} math={iPart.slice(1, -1)} />;
|
| 107 |
+
}
|
| 108 |
|
| 109 |
+
// 3. Citations
|
| 110 |
+
const citeSplit = iPart.split(/\[(\d{4}\.\d{4,5})\]/g);
|
| 111 |
+
return citeSplit.map((cPart, k) => {
|
| 112 |
+
if (/^\d{4}\.\d{4,5}$/.test(cPart)) {
|
| 113 |
+
return <CitationBadge key={`${i}-${j}-${k}`} id={cPart} />;
|
| 114 |
+
}
|
| 115 |
+
return <span key={`${i}-${j}-${k}`} style={{ whiteSpace: "pre-wrap" }}>{cPart}</span>;
|
| 116 |
+
});
|
| 117 |
+
});
|
| 118 |
+
});
|
| 119 |
+
};
|
| 120 |
|
| 121 |
+
const MessageRenderer = ({ content }: { content: string }) => {
|
| 122 |
+
// Split by code blocks first
|
| 123 |
+
const parts = content.split(/(```[\s\S]*?```)/g);
|
| 124 |
return (
|
| 125 |
+
<div style={{ lineHeight: 1.6 }}>
|
| 126 |
+
{parts.map((part, i) => {
|
| 127 |
+
if (part.startsWith('```') && part.endsWith('```')) {
|
| 128 |
+
const match = part.match(/```(\w+)?\n([\s\S]*?)```/);
|
| 129 |
+
if (match) {
|
| 130 |
+
return <CodeBlock key={i} language={match[1]} code={match[2]} />;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
return <div key={i}>{renderTextWithMathAndCitations(part)}</div>;
|
| 134 |
+
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
| 136 |
);
|
| 137 |
+
};
|
| 138 |
|
| 139 |
+
// --- Feedback Component ---
|
| 140 |
+
const FeedbackRow = ({ query, time, citationsCount, model }: { query: string, time: number, citationsCount: number, model: string }) => {
|
| 141 |
+
const [rating, setRating] = useState(0);
|
| 142 |
+
const [hoverRating, setHoverRating] = useState(0);
|
| 143 |
+
const [thumbs, setThumbs] = useState<"up"|"down"|null>(null);
|
| 144 |
+
const [comment, setComment] = useState("");
|
| 145 |
+
const [submitted, setSubmitted] = useState(false);
|
| 146 |
+
|
| 147 |
+
const handleSubmit = async () => {
|
| 148 |
+
try {
|
| 149 |
+
await fetch(`${API_URL}/feedback`, {
|
| 150 |
+
method: "POST",
|
| 151 |
+
headers: { "Content-Type": "application/json" },
|
| 152 |
+
body: JSON.stringify({ query, rating, thumbs, comment, model_used: model, citations_count: citationsCount, total_time_ms: time })
|
| 153 |
+
});
|
| 154 |
+
} catch (e) {
|
| 155 |
+
console.error(e);
|
| 156 |
}
|
| 157 |
+
setSubmitted(true);
|
| 158 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
if (submitted) return <div style={{ fontSize: "0.85rem", color: "var(--success)", marginTop: "12px" }}><Check size={14} style={{ display: 'inline', marginRight: '4px' }} />Thank you for your feedback!</div>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
return (
|
| 163 |
+
<div className="feedback-row">
|
| 164 |
+
<div style={{ display: "flex", gap: "4px" }}>
|
| 165 |
+
{[1,2,3,4,5].map(star => (
|
| 166 |
+
<Star
|
| 167 |
+
key={star}
|
| 168 |
+
size={18}
|
| 169 |
+
className={`star-btn ${(hoverRating || rating) >= star ? 'active' : ''}`}
|
| 170 |
+
onMouseEnter={() => setHoverRating(star)}
|
| 171 |
+
onMouseLeave={() => setHoverRating(0)}
|
| 172 |
+
onClick={() => setRating(star)}
|
| 173 |
+
fill={(hoverRating || rating) >= star ? "#f59e0b" : "transparent"}
|
| 174 |
+
/>
|
| 175 |
+
))}
|
| 176 |
+
</div>
|
| 177 |
+
<div style={{ display: "flex", gap: "8px", borderLeft: "1px solid rgba(255,255,255,0.1)", paddingLeft: "12px" }}>
|
| 178 |
+
<ThumbsUp size={18} className={`star-btn ${thumbs === 'up' ? 'active' : ''}`} onClick={() => setThumbs('up')} />
|
| 179 |
+
<ThumbsDown size={18} className={`star-btn ${thumbs === 'down' ? 'active' : ''}`} onClick={() => setThumbs('down')} />
|
| 180 |
+
</div>
|
| 181 |
+
{(rating > 0 || thumbs) && (
|
| 182 |
+
<>
|
| 183 |
+
<input
|
| 184 |
+
type="text"
|
| 185 |
+
className="feedback-input"
|
| 186 |
+
placeholder="Tell us more (optional)"
|
| 187 |
+
value={comment}
|
| 188 |
+
onChange={(e) => setComment(e.target.value)}
|
| 189 |
+
/>
|
| 190 |
+
<button className="feedback-submit" onClick={handleSubmit}>Submit</button>
|
| 191 |
+
</>
|
| 192 |
+
)}
|
| 193 |
+
</div>
|
| 194 |
+
);
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
export default function App() {
|
| 198 |
+
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
| 199 |
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
| 200 |
+
const [query, setQuery] = useState("");
|
| 201 |
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
| 202 |
+
const [isStreaming, setIsStreaming] = useState(false);
|
| 203 |
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
| 204 |
const [topK, setTopK] = useState(5);
|
| 205 |
const [category, setCategory] = useState("All");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
+
const chatEndRef = useRef<HTMLDivElement>(null);
|
| 208 |
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 209 |
|
| 210 |
+
// Load from local storage
|
| 211 |
useEffect(() => {
|
| 212 |
+
const stored = localStorage.getItem("rp_history");
|
| 213 |
+
if (stored) {
|
| 214 |
+
setSessions(JSON.parse(stored));
|
| 215 |
}
|
| 216 |
+
}, []);
|
| 217 |
|
| 218 |
+
// Save to local storage
|
| 219 |
useEffect(() => {
|
| 220 |
+
if (sessions.length > 0) {
|
| 221 |
+
localStorage.setItem("rp_history", JSON.stringify(sessions));
|
| 222 |
+
}
|
| 223 |
+
}, [sessions]);
|
| 224 |
|
| 225 |
+
// Auto resize textarea
|
| 226 |
+
useEffect(() => {
|
| 227 |
+
if (textareaRef.current) {
|
| 228 |
+
textareaRef.current.style.height = "auto";
|
| 229 |
+
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
+
}, [query]);
|
| 232 |
+
|
| 233 |
+
// Scroll to bottom
|
| 234 |
+
const scrollToBottom = () => {
|
| 235 |
+
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
| 236 |
};
|
| 237 |
|
| 238 |
+
useEffect(() => {
|
| 239 |
+
scrollToBottom();
|
| 240 |
+
}, [sessions, activeSessionId, isStreaming]);
|
| 241 |
+
|
| 242 |
+
const activeSession = sessions.find(s => s.id === activeSessionId);
|
| 243 |
+
const currentMessages = activeSession?.messages || [];
|
| 244 |
|
| 245 |
+
const handleNewChat = () => {
|
| 246 |
+
setActiveSessionId(null);
|
| 247 |
+
setSidebarOpen(false);
|
| 248 |
+
};
|
| 249 |
+
|
| 250 |
+
const handleSend = async () => {
|
| 251 |
+
if (!query.trim() || isStreaming) return;
|
| 252 |
+
|
| 253 |
+
let sessionId = activeSessionId;
|
| 254 |
+
if (!sessionId) {
|
| 255 |
+
sessionId = Date.now().toString();
|
| 256 |
+
const newSession: ChatSession = {
|
| 257 |
+
id: sessionId,
|
| 258 |
+
title: query.trim().substring(0, 50) + (query.length > 50 ? "..." : ""),
|
| 259 |
+
messages: [],
|
| 260 |
+
timestamp: Date.now()
|
| 261 |
};
|
| 262 |
+
setSessions(prev => [newSession, ...prev]);
|
| 263 |
+
setActiveSessionId(sessionId);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
const userMessage: ChatMessage = {
|
| 267 |
+
id: Date.now().toString(),
|
| 268 |
+
role: "user",
|
| 269 |
+
content: query.trim(),
|
| 270 |
+
timestamp: Date.now()
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
const aiMessageId = (Date.now() + 1).toString();
|
| 274 |
+
const placeholderAiMessage: ChatMessage = {
|
| 275 |
+
id: aiMessageId,
|
| 276 |
+
role: "assistant",
|
| 277 |
+
content: "",
|
| 278 |
+
timestamp: Date.now() + 1
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
setSessions(prev => prev.map(s => {
|
| 282 |
+
if (s.id === sessionId) {
|
| 283 |
+
return { ...s, messages: [...s.messages, userMessage, placeholderAiMessage] };
|
| 284 |
+
}
|
| 285 |
+
return s;
|
| 286 |
+
}));
|
| 287 |
+
|
| 288 |
+
const originalQuery = query;
|
| 289 |
+
setQuery("");
|
| 290 |
+
setIsStreaming(true);
|
| 291 |
|
| 292 |
+
try {
|
| 293 |
+
const res = await fetch(`${API_URL}/query/stream`, {
|
| 294 |
method: "POST",
|
| 295 |
headers: { "Content-Type": "application/json" },
|
| 296 |
+
body: JSON.stringify({
|
| 297 |
+
question: originalQuery,
|
| 298 |
+
top_k: topK,
|
| 299 |
+
filter_category: category === "All" ? undefined : category
|
| 300 |
+
})
|
| 301 |
});
|
| 302 |
|
| 303 |
+
if (!res.ok || !res.body) throw new Error("Stream failed");
|
| 304 |
+
|
| 305 |
+
const reader = res.body.getReader();
|
| 306 |
+
const decoder = new TextDecoder();
|
| 307 |
+
|
| 308 |
+
let accumulatedContent = "";
|
| 309 |
+
|
| 310 |
+
while (true) {
|
| 311 |
+
const { done, value } = await reader.read();
|
| 312 |
+
if (done) break;
|
| 313 |
+
|
| 314 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 315 |
+
const lines = chunk.split("\n");
|
| 316 |
+
|
| 317 |
+
for (const line of lines) {
|
| 318 |
+
if (line.startsWith("data: ")) {
|
| 319 |
+
try {
|
| 320 |
+
const data = JSON.parse(line.slice(6));
|
| 321 |
+
if (data.done) {
|
| 322 |
+
// Final update with citations + timing
|
| 323 |
+
setSessions(prev => prev.map(s => {
|
| 324 |
+
if (s.id === sessionId) {
|
| 325 |
+
return {
|
| 326 |
+
...s,
|
| 327 |
+
messages: s.messages.map(m =>
|
| 328 |
+
m.id === aiMessageId
|
| 329 |
+
? { ...m, citations: data.citations, timing: data.timing, model_used: data.model_used }
|
| 330 |
+
: m
|
| 331 |
+
)
|
| 332 |
+
};
|
| 333 |
+
}
|
| 334 |
+
return s;
|
| 335 |
+
}));
|
| 336 |
+
} else if (data.token) {
|
| 337 |
+
accumulatedContent += data.token;
|
| 338 |
+
// Update streaming content
|
| 339 |
+
setSessions(prev => prev.map(s => {
|
| 340 |
+
if (s.id === sessionId) {
|
| 341 |
+
return {
|
| 342 |
+
...s,
|
| 343 |
+
messages: s.messages.map(m =>
|
| 344 |
+
m.id === aiMessageId
|
| 345 |
+
? { ...m, content: accumulatedContent }
|
| 346 |
+
: m
|
| 347 |
+
)
|
| 348 |
+
};
|
| 349 |
+
}
|
| 350 |
+
return s;
|
| 351 |
+
}));
|
| 352 |
+
// scrollToBottom immediately inside the stream
|
| 353 |
+
chatEndRef.current?.scrollIntoView();
|
| 354 |
+
}
|
| 355 |
+
} catch (e) {
|
| 356 |
+
console.error("Parse error:", e);
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
} catch (error) {
|
| 362 |
+
console.error(error);
|
| 363 |
+
setSessions(prev => prev.map(s => {
|
| 364 |
+
if (s.id === sessionId) {
|
| 365 |
+
return {
|
| 366 |
+
...s,
|
| 367 |
+
messages: s.messages.map(m =>
|
| 368 |
+
m.id === aiMessageId
|
| 369 |
+
? { ...m, content: "Error: Could not connect to API or request failed." }
|
| 370 |
+
: m
|
| 371 |
+
)
|
| 372 |
+
};
|
| 373 |
+
}
|
| 374 |
+
return s;
|
| 375 |
+
}));
|
| 376 |
} finally {
|
| 377 |
+
setIsStreaming(false);
|
| 378 |
}
|
| 379 |
};
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
return (
|
| 382 |
+
<div className="layout-wrapper" style={{ background: "var(--bg)", color: "var(--text-main)" }}>
|
| 383 |
+
{/* Mobile Header Menu */}
|
| 384 |
+
<div className="hamburger" onClick={() => setSidebarOpen(true)}>
|
| 385 |
+
<PanelLeft size={24} color="#fff" />
|
| 386 |
+
</div>
|
| 387 |
+
|
| 388 |
+
{/* Sidebar */}
|
| 389 |
+
<div className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
|
| 390 |
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '24px' }}>
|
| 391 |
+
<div className="brand" style={{ fontSize: '1.1rem', gap: '8px' }}>
|
| 392 |
+
<Brain size={20} color="var(--accent)" /> ResearchPilot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
</div>
|
| 394 |
+
{sidebarOpen && (
|
| 395 |
+
<PanelLeftClose size={20} color="#fff" onClick={() => setSidebarOpen(false)} style={{ cursor: 'pointer' }} />
|
| 396 |
+
)}
|
| 397 |
+
</div>
|
| 398 |
+
|
| 399 |
+
<button className="new-chat-btn" onClick={handleNewChat}>
|
| 400 |
+
<Plus size={16} /> New Chat
|
| 401 |
+
</button>
|
| 402 |
+
|
| 403 |
+
<div style={{ flex: 1, overflowY: 'auto' }}>
|
| 404 |
+
<div style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-muted)', marginBottom: '12px', textTransform: 'uppercase' }}>Recent</div>
|
| 405 |
+
{sessions.map(s => (
|
| 406 |
+
<div
|
| 407 |
+
key={s.id}
|
| 408 |
+
className={`history-item ${activeSessionId === s.id ? 'active' : ''}`}
|
| 409 |
+
onClick={() => { setActiveSessionId(s.id); setSidebarOpen(false); }}
|
| 410 |
+
>
|
| 411 |
+
{s.title}
|
| 412 |
</div>
|
| 413 |
+
))}
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
|
| 417 |
+
{/* Overlay for mobile sidebar */}
|
| 418 |
+
{sidebarOpen && <div className="sidebar-overlay" onClick={() => setSidebarOpen(false)} />}
|
| 419 |
+
|
| 420 |
+
{/* Main Area */}
|
| 421 |
+
<div className="main-chat-area">
|
| 422 |
+
<div className="chat-container">
|
| 423 |
+
{currentMessages.length === 0 ? (
|
| 424 |
+
<div style={{ margin: 'auto', textAlign: 'center', opacity: 0.5, maxWidth: '400px' }}>
|
| 425 |
+
<Brain size={48} style={{ margin: '0 auto 16px auto', display: 'block' }} />
|
| 426 |
+
<h2>How can I help you with ML research?</h2>
|
| 427 |
+
</div>
|
| 428 |
+
) : (
|
| 429 |
+
currentMessages.map((msg, i) => (
|
| 430 |
+
<div key={msg.id} className={msg.role === 'user' ? 'message-user' : 'message-ai'}>
|
| 431 |
+
{msg.role === 'user' ? (
|
| 432 |
+
<div style={{ whiteSpace: 'pre-wrap' }}>{msg.content}</div>
|
| 433 |
+
) : (
|
| 434 |
+
<div style={{ width: '100%' }}>
|
| 435 |
+
{/* Name header for AI */}
|
| 436 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px', fontSize: '0.85rem', color: 'var(--accent)', fontWeight: 600 }}>
|
| 437 |
+
<Brain size={16} /> ResearchPilot {msg.model_used && <span style={{fontSize: '0.7rem', color: '#666', background: '#222', padding: '2px 6px', borderRadius: '4px'}}>{msg.model_used}</span>}
|
| 438 |
+
</div>
|
| 439 |
|
| 440 |
+
{ /* Stream logic vs Final logic */
|
| 441 |
+
isStreaming && i === currentMessages.length - 1 ? (
|
| 442 |
+
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>
|
| 443 |
+
{msg.content}
|
| 444 |
+
<span className="blinking-cursor"></span>
|
| 445 |
+
</div>
|
| 446 |
+
) : (
|
| 447 |
+
<>
|
| 448 |
+
<MessageRenderer content={msg.content} />
|
| 449 |
+
{/* Citations section if present */}
|
| 450 |
+
{msg.citations && msg.citations.length > 0 && (
|
| 451 |
+
<div style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
| 452 |
+
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-muted)', marginBottom: '8px' }}>SOURCES</div>
|
| 453 |
+
<div className="citations-grid">
|
| 454 |
+
{msg.citations.map(c => (
|
| 455 |
+
<div key={c.paper_id} className="citation-card">
|
| 456 |
+
<div className="citation-meta">
|
| 457 |
+
<span className="citation-id">{c.paper_id}</span>
|
| 458 |
+
</div>
|
| 459 |
+
<div className="citation-title">{c.title}</div>
|
| 460 |
+
<a href={c.arxiv_url} target="_blank" rel="noopener noreferrer" className="citation-open" title="Open ArXiv PDF">
|
| 461 |
+
<Search size={16} />
|
| 462 |
+
</a>
|
| 463 |
+
</div>
|
| 464 |
+
))}
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
)}
|
| 468 |
+
{/* Feedback Row */}
|
| 469 |
+
{!isStreaming && msg.role === 'assistant' && msg.content && (
|
| 470 |
+
<FeedbackRow
|
| 471 |
+
query={currentMessages[i-1]?.content || ""}
|
| 472 |
+
time={msg.timing?.total_time_ms || 0}
|
| 473 |
+
citationsCount={msg.citations?.length || 0}
|
| 474 |
+
model={msg.model_used || "unknown"}
|
| 475 |
+
/>
|
| 476 |
+
)}
|
| 477 |
+
</>
|
| 478 |
+
)
|
| 479 |
+
}
|
| 480 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
)}
|
| 482 |
+
</div>
|
| 483 |
+
))
|
| 484 |
+
)}
|
| 485 |
+
<div ref={chatEndRef} style={{ height: "1px" }} />
|
| 486 |
+
</div>
|
| 487 |
+
|
| 488 |
+
{/* Bottom Input Area */}
|
| 489 |
+
<div className="bottom-input-bar">
|
| 490 |
+
<div className="bottom-input-bar-inner">
|
| 491 |
+
{/* Settings Popup inline */}
|
| 492 |
+
<AnimatePresence>
|
| 493 |
+
{settingsOpen && (
|
| 494 |
+
<motion.div
|
| 495 |
+
initial={{ opacity: 0, y: 10 }}
|
| 496 |
+
animate={{ opacity: 1, y: 0 }}
|
| 497 |
+
exit={{ opacity: 0, y: 10 }}
|
| 498 |
+
style={{ background: 'rgba(20,25,35,0.95)', border: '1px solid rgba(255,255,255,0.1)', padding: '12px', borderRadius: '12px', display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '8px' }}
|
| 499 |
+
>
|
| 500 |
+
<select style={{ background: '#000', border: '1px solid #333', color: '#fff', padding: '6px 12px', borderRadius: '6px' }} value={topK} onChange={(e) => setTopK(Number(e.target.value))}>
|
| 501 |
+
<option value={3}>Top 3</option>
|
| 502 |
+
<option value={5}>Top 5</option>
|
| 503 |
+
<option value={10}>Top 10</option>
|
| 504 |
+
</select>
|
| 505 |
+
<select style={{ background: '#000', border: '1px solid #333', color: '#fff', padding: '6px 12px', borderRadius: '6px' }} value={category} onChange={(e) => setCategory(e.target.value)}>
|
| 506 |
+
<option value="All">All Topics</option>
|
| 507 |
+
<option value="cs.LG">cs.LG</option>
|
| 508 |
+
<option value="cs.AI">cs.AI</option>
|
| 509 |
+
</select>
|
| 510 |
+
</motion.div>
|
| 511 |
+
)}
|
| 512 |
+
</AnimatePresence>
|
| 513 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
<div className="chat-input-wrapper">
|
| 515 |
+
<button onClick={() => setSettingsOpen(!settingsOpen)} style={{ color: settingsOpen ? 'var(--accent)' : 'var(--text-muted)' }}>
|
| 516 |
+
<Settings2 size={20} />
|
| 517 |
+
</button>
|
| 518 |
+
<textarea
|
| 519 |
ref={textareaRef}
|
| 520 |
+
value={query}
|
| 521 |
+
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
onKeyDown={(e) => {
|
| 523 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 524 |
e.preventDefault();
|
| 525 |
+
handleSend();
|
| 526 |
}
|
| 527 |
}}
|
| 528 |
+
placeholder="Message ResearchPilot..."
|
| 529 |
+
className="chat-input"
|
| 530 |
+
rows={1}
|
| 531 |
/>
|
| 532 |
+
<button onClick={handleSend} disabled={!query.trim() || isStreaming} className="send-btn">
|
| 533 |
+
<Send size={18} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
</button>
|
| 535 |
</div>
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
|
| 540 |
+
<div className="luminous-grid" />
|
| 541 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
);
|
| 543 |
}
|
frontend-next/package-lock.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
| 16 |
"react": "19.2.4",
|
| 17 |
"react-dom": "19.2.4",
|
| 18 |
"react-katex": "^3.1.0",
|
|
|
|
| 19 |
"tailwind-merge": "^3.5.0"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
|
@@ -25,6 +26,7 @@
|
|
| 25 |
"@types/react": "^19",
|
| 26 |
"@types/react-dom": "^19",
|
| 27 |
"@types/react-katex": "^3.0.4",
|
|
|
|
| 28 |
"eslint": "^9",
|
| 29 |
"eslint-config-next": "16.2.2",
|
| 30 |
"tailwindcss": "^4",
|
|
@@ -236,6 +238,15 @@
|
|
| 236 |
"node": ">=6.0.0"
|
| 237 |
}
|
| 238 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
"node_modules/@babel/template": {
|
| 240 |
"version": "7.28.6",
|
| 241 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
|
@@ -1611,6 +1622,15 @@
|
|
| 1611 |
"dev": true,
|
| 1612 |
"license": "MIT"
|
| 1613 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
"node_modules/@types/json-schema": {
|
| 1615 |
"version": "7.0.15",
|
| 1616 |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
@@ -1642,6 +1662,12 @@
|
|
| 1642 |
"undici-types": "~6.21.0"
|
| 1643 |
}
|
| 1644 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1645 |
"node_modules/@types/react": {
|
| 1646 |
"version": "19.2.14",
|
| 1647 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
|
@@ -1672,6 +1698,22 @@
|
|
| 1672 |
"@types/react": "*"
|
| 1673 |
}
|
| 1674 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1675 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 1676 |
"version": "8.58.0",
|
| 1677 |
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
|
|
@@ -2720,6 +2762,36 @@
|
|
| 2720 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 2721 |
}
|
| 2722 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2723 |
"node_modules/client-only": {
|
| 2724 |
"version": "0.0.1",
|
| 2725 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
@@ -2755,6 +2827,16 @@
|
|
| 2755 |
"dev": true,
|
| 2756 |
"license": "MIT"
|
| 2757 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2758 |
"node_modules/commander": {
|
| 2759 |
"version": "8.3.0",
|
| 2760 |
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
|
@@ -2879,6 +2961,19 @@
|
|
| 2879 |
}
|
| 2880 |
}
|
| 2881 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2882 |
"node_modules/deep-is": {
|
| 2883 |
"version": "0.1.4",
|
| 2884 |
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
|
@@ -3656,6 +3751,19 @@
|
|
| 3656 |
"reusify": "^1.0.4"
|
| 3657 |
}
|
| 3658 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3659 |
"node_modules/file-entry-cache": {
|
| 3660 |
"version": "8.0.0",
|
| 3661 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
|
@@ -3736,6 +3844,14 @@
|
|
| 3736 |
"url": "https://github.com/sponsors/ljharb"
|
| 3737 |
}
|
| 3738 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3739 |
"node_modules/framer-motion": {
|
| 3740 |
"version": "12.38.0",
|
| 3741 |
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
|
@@ -4051,6 +4167,36 @@
|
|
| 4051 |
"node": ">= 0.4"
|
| 4052 |
}
|
| 4053 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4054 |
"node_modules/hermes-estree": {
|
| 4055 |
"version": "0.25.1",
|
| 4056 |
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
|
@@ -4068,6 +4214,21 @@
|
|
| 4068 |
"hermes-estree": "0.25.1"
|
| 4069 |
}
|
| 4070 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4071 |
"node_modules/ignore": {
|
| 4072 |
"version": "5.3.2",
|
| 4073 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
@@ -4120,6 +4281,30 @@
|
|
| 4120 |
"node": ">= 0.4"
|
| 4121 |
}
|
| 4122 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4123 |
"node_modules/is-array-buffer": {
|
| 4124 |
"version": "3.0.5",
|
| 4125 |
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
|
@@ -4278,6 +4463,16 @@
|
|
| 4278 |
"url": "https://github.com/sponsors/ljharb"
|
| 4279 |
}
|
| 4280 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4281 |
"node_modules/is-extglob": {
|
| 4282 |
"version": "2.1.1",
|
| 4283 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
@@ -4337,6 +4532,16 @@
|
|
| 4337 |
"node": ">=0.10.0"
|
| 4338 |
}
|
| 4339 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4340 |
"node_modules/is-map": {
|
| 4341 |
"version": "2.0.3",
|
| 4342 |
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
|
|
@@ -5027,6 +5232,20 @@
|
|
| 5027 |
"loose-envify": "cli.js"
|
| 5028 |
}
|
| 5029 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5030 |
"node_modules/lru-cache": {
|
| 5031 |
"version": "5.1.1",
|
| 5032 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
@@ -5486,6 +5705,31 @@
|
|
| 5486 |
"node": ">=6"
|
| 5487 |
}
|
| 5488 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5489 |
"node_modules/path-exists": {
|
| 5490 |
"version": "4.0.0",
|
| 5491 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
@@ -5581,6 +5825,15 @@
|
|
| 5581 |
"node": ">= 0.8.0"
|
| 5582 |
}
|
| 5583 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5584 |
"node_modules/prop-types": {
|
| 5585 |
"version": "15.8.1",
|
| 5586 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
@@ -5592,6 +5845,16 @@
|
|
| 5592 |
"react-is": "^16.13.1"
|
| 5593 |
}
|
| 5594 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5595 |
"node_modules/punycode": {
|
| 5596 |
"version": "2.3.1",
|
| 5597 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
@@ -5663,6 +5926,26 @@
|
|
| 5663 |
"react": ">=15.3.2 <20"
|
| 5664 |
}
|
| 5665 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5666 |
"node_modules/reflect.getprototypeof": {
|
| 5667 |
"version": "1.0.10",
|
| 5668 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
@@ -5686,6 +5969,22 @@
|
|
| 5686 |
"url": "https://github.com/sponsors/ljharb"
|
| 5687 |
}
|
| 5688 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5689 |
"node_modules/regexp.prototype.flags": {
|
| 5690 |
"version": "1.5.4",
|
| 5691 |
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
|
@@ -6072,6 +6371,16 @@
|
|
| 6072 |
"node": ">=0.10.0"
|
| 6073 |
}
|
| 6074 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6075 |
"node_modules/stable-hash": {
|
| 6076 |
"version": "0.0.5",
|
| 6077 |
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
|
|
|
| 16 |
"react": "19.2.4",
|
| 17 |
"react-dom": "19.2.4",
|
| 18 |
"react-katex": "^3.1.0",
|
| 19 |
+
"react-syntax-highlighter": "^16.1.1",
|
| 20 |
"tailwind-merge": "^3.5.0"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
|
|
|
| 26 |
"@types/react": "^19",
|
| 27 |
"@types/react-dom": "^19",
|
| 28 |
"@types/react-katex": "^3.0.4",
|
| 29 |
+
"@types/react-syntax-highlighter": "^15.5.13",
|
| 30 |
"eslint": "^9",
|
| 31 |
"eslint-config-next": "16.2.2",
|
| 32 |
"tailwindcss": "^4",
|
|
|
|
| 238 |
"node": ">=6.0.0"
|
| 239 |
}
|
| 240 |
},
|
| 241 |
+
"node_modules/@babel/runtime": {
|
| 242 |
+
"version": "7.29.2",
|
| 243 |
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
| 244 |
+
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
| 245 |
+
"license": "MIT",
|
| 246 |
+
"engines": {
|
| 247 |
+
"node": ">=6.9.0"
|
| 248 |
+
}
|
| 249 |
+
},
|
| 250 |
"node_modules/@babel/template": {
|
| 251 |
"version": "7.28.6",
|
| 252 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
|
|
|
| 1622 |
"dev": true,
|
| 1623 |
"license": "MIT"
|
| 1624 |
},
|
| 1625 |
+
"node_modules/@types/hast": {
|
| 1626 |
+
"version": "3.0.4",
|
| 1627 |
+
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
| 1628 |
+
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
| 1629 |
+
"license": "MIT",
|
| 1630 |
+
"dependencies": {
|
| 1631 |
+
"@types/unist": "*"
|
| 1632 |
+
}
|
| 1633 |
+
},
|
| 1634 |
"node_modules/@types/json-schema": {
|
| 1635 |
"version": "7.0.15",
|
| 1636 |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
|
|
| 1662 |
"undici-types": "~6.21.0"
|
| 1663 |
}
|
| 1664 |
},
|
| 1665 |
+
"node_modules/@types/prismjs": {
|
| 1666 |
+
"version": "1.26.6",
|
| 1667 |
+
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
|
| 1668 |
+
"integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
|
| 1669 |
+
"license": "MIT"
|
| 1670 |
+
},
|
| 1671 |
"node_modules/@types/react": {
|
| 1672 |
"version": "19.2.14",
|
| 1673 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
|
|
|
| 1698 |
"@types/react": "*"
|
| 1699 |
}
|
| 1700 |
},
|
| 1701 |
+
"node_modules/@types/react-syntax-highlighter": {
|
| 1702 |
+
"version": "15.5.13",
|
| 1703 |
+
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
| 1704 |
+
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
| 1705 |
+
"dev": true,
|
| 1706 |
+
"license": "MIT",
|
| 1707 |
+
"dependencies": {
|
| 1708 |
+
"@types/react": "*"
|
| 1709 |
+
}
|
| 1710 |
+
},
|
| 1711 |
+
"node_modules/@types/unist": {
|
| 1712 |
+
"version": "3.0.3",
|
| 1713 |
+
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
| 1714 |
+
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
| 1715 |
+
"license": "MIT"
|
| 1716 |
+
},
|
| 1717 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 1718 |
"version": "8.58.0",
|
| 1719 |
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
|
|
|
|
| 2762 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 2763 |
}
|
| 2764 |
},
|
| 2765 |
+
"node_modules/character-entities": {
|
| 2766 |
+
"version": "2.0.2",
|
| 2767 |
+
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
| 2768 |
+
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
|
| 2769 |
+
"license": "MIT",
|
| 2770 |
+
"funding": {
|
| 2771 |
+
"type": "github",
|
| 2772 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 2773 |
+
}
|
| 2774 |
+
},
|
| 2775 |
+
"node_modules/character-entities-legacy": {
|
| 2776 |
+
"version": "3.0.0",
|
| 2777 |
+
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
| 2778 |
+
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
| 2779 |
+
"license": "MIT",
|
| 2780 |
+
"funding": {
|
| 2781 |
+
"type": "github",
|
| 2782 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 2783 |
+
}
|
| 2784 |
+
},
|
| 2785 |
+
"node_modules/character-reference-invalid": {
|
| 2786 |
+
"version": "2.0.1",
|
| 2787 |
+
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
|
| 2788 |
+
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
|
| 2789 |
+
"license": "MIT",
|
| 2790 |
+
"funding": {
|
| 2791 |
+
"type": "github",
|
| 2792 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 2793 |
+
}
|
| 2794 |
+
},
|
| 2795 |
"node_modules/client-only": {
|
| 2796 |
"version": "0.0.1",
|
| 2797 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
|
|
| 2827 |
"dev": true,
|
| 2828 |
"license": "MIT"
|
| 2829 |
},
|
| 2830 |
+
"node_modules/comma-separated-tokens": {
|
| 2831 |
+
"version": "2.0.3",
|
| 2832 |
+
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
| 2833 |
+
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
| 2834 |
+
"license": "MIT",
|
| 2835 |
+
"funding": {
|
| 2836 |
+
"type": "github",
|
| 2837 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 2838 |
+
}
|
| 2839 |
+
},
|
| 2840 |
"node_modules/commander": {
|
| 2841 |
"version": "8.3.0",
|
| 2842 |
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
|
|
|
| 2961 |
}
|
| 2962 |
}
|
| 2963 |
},
|
| 2964 |
+
"node_modules/decode-named-character-reference": {
|
| 2965 |
+
"version": "1.3.0",
|
| 2966 |
+
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
| 2967 |
+
"integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
|
| 2968 |
+
"license": "MIT",
|
| 2969 |
+
"dependencies": {
|
| 2970 |
+
"character-entities": "^2.0.0"
|
| 2971 |
+
},
|
| 2972 |
+
"funding": {
|
| 2973 |
+
"type": "github",
|
| 2974 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 2975 |
+
}
|
| 2976 |
+
},
|
| 2977 |
"node_modules/deep-is": {
|
| 2978 |
"version": "0.1.4",
|
| 2979 |
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
|
|
|
| 3751 |
"reusify": "^1.0.4"
|
| 3752 |
}
|
| 3753 |
},
|
| 3754 |
+
"node_modules/fault": {
|
| 3755 |
+
"version": "1.0.4",
|
| 3756 |
+
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
| 3757 |
+
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
| 3758 |
+
"license": "MIT",
|
| 3759 |
+
"dependencies": {
|
| 3760 |
+
"format": "^0.2.0"
|
| 3761 |
+
},
|
| 3762 |
+
"funding": {
|
| 3763 |
+
"type": "github",
|
| 3764 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3765 |
+
}
|
| 3766 |
+
},
|
| 3767 |
"node_modules/file-entry-cache": {
|
| 3768 |
"version": "8.0.0",
|
| 3769 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
|
|
|
| 3844 |
"url": "https://github.com/sponsors/ljharb"
|
| 3845 |
}
|
| 3846 |
},
|
| 3847 |
+
"node_modules/format": {
|
| 3848 |
+
"version": "0.2.2",
|
| 3849 |
+
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
| 3850 |
+
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
| 3851 |
+
"engines": {
|
| 3852 |
+
"node": ">=0.4.x"
|
| 3853 |
+
}
|
| 3854 |
+
},
|
| 3855 |
"node_modules/framer-motion": {
|
| 3856 |
"version": "12.38.0",
|
| 3857 |
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
|
|
|
| 4167 |
"node": ">= 0.4"
|
| 4168 |
}
|
| 4169 |
},
|
| 4170 |
+
"node_modules/hast-util-parse-selector": {
|
| 4171 |
+
"version": "4.0.0",
|
| 4172 |
+
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
| 4173 |
+
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
| 4174 |
+
"license": "MIT",
|
| 4175 |
+
"dependencies": {
|
| 4176 |
+
"@types/hast": "^3.0.0"
|
| 4177 |
+
},
|
| 4178 |
+
"funding": {
|
| 4179 |
+
"type": "opencollective",
|
| 4180 |
+
"url": "https://opencollective.com/unified"
|
| 4181 |
+
}
|
| 4182 |
+
},
|
| 4183 |
+
"node_modules/hastscript": {
|
| 4184 |
+
"version": "9.0.1",
|
| 4185 |
+
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
| 4186 |
+
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
| 4187 |
+
"license": "MIT",
|
| 4188 |
+
"dependencies": {
|
| 4189 |
+
"@types/hast": "^3.0.0",
|
| 4190 |
+
"comma-separated-tokens": "^2.0.0",
|
| 4191 |
+
"hast-util-parse-selector": "^4.0.0",
|
| 4192 |
+
"property-information": "^7.0.0",
|
| 4193 |
+
"space-separated-tokens": "^2.0.0"
|
| 4194 |
+
},
|
| 4195 |
+
"funding": {
|
| 4196 |
+
"type": "opencollective",
|
| 4197 |
+
"url": "https://opencollective.com/unified"
|
| 4198 |
+
}
|
| 4199 |
+
},
|
| 4200 |
"node_modules/hermes-estree": {
|
| 4201 |
"version": "0.25.1",
|
| 4202 |
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
|
|
|
| 4214 |
"hermes-estree": "0.25.1"
|
| 4215 |
}
|
| 4216 |
},
|
| 4217 |
+
"node_modules/highlight.js": {
|
| 4218 |
+
"version": "10.7.3",
|
| 4219 |
+
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
| 4220 |
+
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
| 4221 |
+
"license": "BSD-3-Clause",
|
| 4222 |
+
"engines": {
|
| 4223 |
+
"node": "*"
|
| 4224 |
+
}
|
| 4225 |
+
},
|
| 4226 |
+
"node_modules/highlightjs-vue": {
|
| 4227 |
+
"version": "1.0.0",
|
| 4228 |
+
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
| 4229 |
+
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
| 4230 |
+
"license": "CC0-1.0"
|
| 4231 |
+
},
|
| 4232 |
"node_modules/ignore": {
|
| 4233 |
"version": "5.3.2",
|
| 4234 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
|
|
| 4281 |
"node": ">= 0.4"
|
| 4282 |
}
|
| 4283 |
},
|
| 4284 |
+
"node_modules/is-alphabetical": {
|
| 4285 |
+
"version": "2.0.1",
|
| 4286 |
+
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
| 4287 |
+
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
|
| 4288 |
+
"license": "MIT",
|
| 4289 |
+
"funding": {
|
| 4290 |
+
"type": "github",
|
| 4291 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4292 |
+
}
|
| 4293 |
+
},
|
| 4294 |
+
"node_modules/is-alphanumerical": {
|
| 4295 |
+
"version": "2.0.1",
|
| 4296 |
+
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
|
| 4297 |
+
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
|
| 4298 |
+
"license": "MIT",
|
| 4299 |
+
"dependencies": {
|
| 4300 |
+
"is-alphabetical": "^2.0.0",
|
| 4301 |
+
"is-decimal": "^2.0.0"
|
| 4302 |
+
},
|
| 4303 |
+
"funding": {
|
| 4304 |
+
"type": "github",
|
| 4305 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4306 |
+
}
|
| 4307 |
+
},
|
| 4308 |
"node_modules/is-array-buffer": {
|
| 4309 |
"version": "3.0.5",
|
| 4310 |
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
|
|
|
| 4463 |
"url": "https://github.com/sponsors/ljharb"
|
| 4464 |
}
|
| 4465 |
},
|
| 4466 |
+
"node_modules/is-decimal": {
|
| 4467 |
+
"version": "2.0.1",
|
| 4468 |
+
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
|
| 4469 |
+
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
|
| 4470 |
+
"license": "MIT",
|
| 4471 |
+
"funding": {
|
| 4472 |
+
"type": "github",
|
| 4473 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4474 |
+
}
|
| 4475 |
+
},
|
| 4476 |
"node_modules/is-extglob": {
|
| 4477 |
"version": "2.1.1",
|
| 4478 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
|
|
| 4532 |
"node": ">=0.10.0"
|
| 4533 |
}
|
| 4534 |
},
|
| 4535 |
+
"node_modules/is-hexadecimal": {
|
| 4536 |
+
"version": "2.0.1",
|
| 4537 |
+
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
|
| 4538 |
+
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
|
| 4539 |
+
"license": "MIT",
|
| 4540 |
+
"funding": {
|
| 4541 |
+
"type": "github",
|
| 4542 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4543 |
+
}
|
| 4544 |
+
},
|
| 4545 |
"node_modules/is-map": {
|
| 4546 |
"version": "2.0.3",
|
| 4547 |
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
|
|
|
|
| 5232 |
"loose-envify": "cli.js"
|
| 5233 |
}
|
| 5234 |
},
|
| 5235 |
+
"node_modules/lowlight": {
|
| 5236 |
+
"version": "1.20.0",
|
| 5237 |
+
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
| 5238 |
+
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
| 5239 |
+
"license": "MIT",
|
| 5240 |
+
"dependencies": {
|
| 5241 |
+
"fault": "^1.0.0",
|
| 5242 |
+
"highlight.js": "~10.7.0"
|
| 5243 |
+
},
|
| 5244 |
+
"funding": {
|
| 5245 |
+
"type": "github",
|
| 5246 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 5247 |
+
}
|
| 5248 |
+
},
|
| 5249 |
"node_modules/lru-cache": {
|
| 5250 |
"version": "5.1.1",
|
| 5251 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
|
|
| 5705 |
"node": ">=6"
|
| 5706 |
}
|
| 5707 |
},
|
| 5708 |
+
"node_modules/parse-entities": {
|
| 5709 |
+
"version": "4.0.2",
|
| 5710 |
+
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
| 5711 |
+
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
|
| 5712 |
+
"license": "MIT",
|
| 5713 |
+
"dependencies": {
|
| 5714 |
+
"@types/unist": "^2.0.0",
|
| 5715 |
+
"character-entities-legacy": "^3.0.0",
|
| 5716 |
+
"character-reference-invalid": "^2.0.0",
|
| 5717 |
+
"decode-named-character-reference": "^1.0.0",
|
| 5718 |
+
"is-alphanumerical": "^2.0.0",
|
| 5719 |
+
"is-decimal": "^2.0.0",
|
| 5720 |
+
"is-hexadecimal": "^2.0.0"
|
| 5721 |
+
},
|
| 5722 |
+
"funding": {
|
| 5723 |
+
"type": "github",
|
| 5724 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 5725 |
+
}
|
| 5726 |
+
},
|
| 5727 |
+
"node_modules/parse-entities/node_modules/@types/unist": {
|
| 5728 |
+
"version": "2.0.11",
|
| 5729 |
+
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
| 5730 |
+
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
| 5731 |
+
"license": "MIT"
|
| 5732 |
+
},
|
| 5733 |
"node_modules/path-exists": {
|
| 5734 |
"version": "4.0.0",
|
| 5735 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
|
|
| 5825 |
"node": ">= 0.8.0"
|
| 5826 |
}
|
| 5827 |
},
|
| 5828 |
+
"node_modules/prismjs": {
|
| 5829 |
+
"version": "1.30.0",
|
| 5830 |
+
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
| 5831 |
+
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
| 5832 |
+
"license": "MIT",
|
| 5833 |
+
"engines": {
|
| 5834 |
+
"node": ">=6"
|
| 5835 |
+
}
|
| 5836 |
+
},
|
| 5837 |
"node_modules/prop-types": {
|
| 5838 |
"version": "15.8.1",
|
| 5839 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
|
|
| 5845 |
"react-is": "^16.13.1"
|
| 5846 |
}
|
| 5847 |
},
|
| 5848 |
+
"node_modules/property-information": {
|
| 5849 |
+
"version": "7.1.0",
|
| 5850 |
+
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
| 5851 |
+
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
| 5852 |
+
"license": "MIT",
|
| 5853 |
+
"funding": {
|
| 5854 |
+
"type": "github",
|
| 5855 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 5856 |
+
}
|
| 5857 |
+
},
|
| 5858 |
"node_modules/punycode": {
|
| 5859 |
"version": "2.3.1",
|
| 5860 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
|
|
| 5926 |
"react": ">=15.3.2 <20"
|
| 5927 |
}
|
| 5928 |
},
|
| 5929 |
+
"node_modules/react-syntax-highlighter": {
|
| 5930 |
+
"version": "16.1.1",
|
| 5931 |
+
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz",
|
| 5932 |
+
"integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==",
|
| 5933 |
+
"license": "MIT",
|
| 5934 |
+
"dependencies": {
|
| 5935 |
+
"@babel/runtime": "^7.28.4",
|
| 5936 |
+
"highlight.js": "^10.4.1",
|
| 5937 |
+
"highlightjs-vue": "^1.0.0",
|
| 5938 |
+
"lowlight": "^1.17.0",
|
| 5939 |
+
"prismjs": "^1.30.0",
|
| 5940 |
+
"refractor": "^5.0.0"
|
| 5941 |
+
},
|
| 5942 |
+
"engines": {
|
| 5943 |
+
"node": ">= 16.20.2"
|
| 5944 |
+
},
|
| 5945 |
+
"peerDependencies": {
|
| 5946 |
+
"react": ">= 0.14.0"
|
| 5947 |
+
}
|
| 5948 |
+
},
|
| 5949 |
"node_modules/reflect.getprototypeof": {
|
| 5950 |
"version": "1.0.10",
|
| 5951 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
|
|
| 5969 |
"url": "https://github.com/sponsors/ljharb"
|
| 5970 |
}
|
| 5971 |
},
|
| 5972 |
+
"node_modules/refractor": {
|
| 5973 |
+
"version": "5.0.0",
|
| 5974 |
+
"resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
|
| 5975 |
+
"integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
|
| 5976 |
+
"license": "MIT",
|
| 5977 |
+
"dependencies": {
|
| 5978 |
+
"@types/hast": "^3.0.0",
|
| 5979 |
+
"@types/prismjs": "^1.0.0",
|
| 5980 |
+
"hastscript": "^9.0.0",
|
| 5981 |
+
"parse-entities": "^4.0.0"
|
| 5982 |
+
},
|
| 5983 |
+
"funding": {
|
| 5984 |
+
"type": "github",
|
| 5985 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 5986 |
+
}
|
| 5987 |
+
},
|
| 5988 |
"node_modules/regexp.prototype.flags": {
|
| 5989 |
"version": "1.5.4",
|
| 5990 |
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
|
|
|
| 6371 |
"node": ">=0.10.0"
|
| 6372 |
}
|
| 6373 |
},
|
| 6374 |
+
"node_modules/space-separated-tokens": {
|
| 6375 |
+
"version": "2.0.2",
|
| 6376 |
+
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
| 6377 |
+
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
| 6378 |
+
"license": "MIT",
|
| 6379 |
+
"funding": {
|
| 6380 |
+
"type": "github",
|
| 6381 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6382 |
+
}
|
| 6383 |
+
},
|
| 6384 |
"node_modules/stable-hash": {
|
| 6385 |
"version": "0.0.5",
|
| 6386 |
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
frontend-next/package.json
CHANGED
|
@@ -17,6 +17,7 @@
|
|
| 17 |
"react": "19.2.4",
|
| 18 |
"react-dom": "19.2.4",
|
| 19 |
"react-katex": "^3.1.0",
|
|
|
|
| 20 |
"tailwind-merge": "^3.5.0"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
|
@@ -26,6 +27,7 @@
|
|
| 26 |
"@types/react": "^19",
|
| 27 |
"@types/react-dom": "^19",
|
| 28 |
"@types/react-katex": "^3.0.4",
|
|
|
|
| 29 |
"eslint": "^9",
|
| 30 |
"eslint-config-next": "16.2.2",
|
| 31 |
"tailwindcss": "^4",
|
|
|
|
| 17 |
"react": "19.2.4",
|
| 18 |
"react-dom": "19.2.4",
|
| 19 |
"react-katex": "^3.1.0",
|
| 20 |
+
"react-syntax-highlighter": "^16.1.1",
|
| 21 |
"tailwind-merge": "^3.5.0"
|
| 22 |
},
|
| 23 |
"devDependencies": {
|
|
|
|
| 27 |
"@types/react": "^19",
|
| 28 |
"@types/react-dom": "^19",
|
| 29 |
"@types/react-katex": "^3.0.4",
|
| 30 |
+
"@types/react-syntax-highlighter": "^15.5.13",
|
| 31 |
"eslint": "^9",
|
| 32 |
"eslint-config-next": "16.2.2",
|
| 33 |
"tailwindcss": "^4",
|
src/api/main.py
CHANGED
|
@@ -26,7 +26,10 @@ from contextlib import asynccontextmanager
|
|
| 26 |
|
| 27 |
from fastapi import FastAPI, HTTPException, Request
|
| 28 |
from fastapi.middleware.cors import CORSMiddleware
|
| 29 |
-
from fastapi.responses import JSONResponse
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
from src.api.schemas import (
|
| 32 |
QueryRequest,
|
|
@@ -35,6 +38,15 @@ from src.api.schemas import (
|
|
| 35 |
HealthResponse,
|
| 36 |
ErrorResponse,
|
| 37 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
from src.rag.pipeline import RAGPipeline
|
| 39 |
from src.utils.logger import setup_logger, get_logger
|
| 40 |
|
|
@@ -149,6 +161,36 @@ async def health_check(request: Request) -> HealthResponse:
|
|
| 149 |
version = "1.0.0",
|
| 150 |
)
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
@app.post(
|
| 154 |
"/query",
|
|
|
|
| 26 |
|
| 27 |
from fastapi import FastAPI, HTTPException, Request
|
| 28 |
from fastapi.middleware.cors import CORSMiddleware
|
| 29 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 30 |
+
from pydantic import BaseModel
|
| 31 |
+
import json
|
| 32 |
+
import os
|
| 33 |
|
| 34 |
from src.api.schemas import (
|
| 35 |
QueryRequest,
|
|
|
|
| 38 |
HealthResponse,
|
| 39 |
ErrorResponse,
|
| 40 |
)
|
| 41 |
+
|
| 42 |
+
class FeedbackRequest(BaseModel):
|
| 43 |
+
query: str
|
| 44 |
+
rating: int
|
| 45 |
+
thumbs: str | None = None
|
| 46 |
+
comment: str
|
| 47 |
+
model_used: str
|
| 48 |
+
citations_count: int
|
| 49 |
+
total_time_ms: float
|
| 50 |
from src.rag.pipeline import RAGPipeline
|
| 51 |
from src.utils.logger import setup_logger, get_logger
|
| 52 |
|
|
|
|
| 161 |
version = "1.0.0",
|
| 162 |
)
|
| 163 |
|
| 164 |
+
@app.post(
|
| 165 |
+
"/query/stream",
|
| 166 |
+
summary = "Stream query research papers",
|
| 167 |
+
tags = ["RAG"],
|
| 168 |
+
)
|
| 169 |
+
async def stream_query_papers(
|
| 170 |
+
request: Request,
|
| 171 |
+
query_input: QueryRequest,
|
| 172 |
+
):
|
| 173 |
+
pipeline = request.app.state.rag_pipeline
|
| 174 |
+
return StreamingResponse(
|
| 175 |
+
pipeline.stream_query(
|
| 176 |
+
question = query_input.question,
|
| 177 |
+
top_k = query_input.top_k,
|
| 178 |
+
filter_category = query_input.filter_category,
|
| 179 |
+
filter_year_gte = query_input.filter_year_gte,
|
| 180 |
+
),
|
| 181 |
+
media_type="text/event-stream"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
@app.post(
|
| 185 |
+
"/feedback",
|
| 186 |
+
summary = "Submit feedback",
|
| 187 |
+
tags = ["System"],
|
| 188 |
+
)
|
| 189 |
+
async def submit_feedback(feedback: FeedbackRequest):
|
| 190 |
+
os.makedirs("logs", exist_ok=True)
|
| 191 |
+
with open("logs/feedback.jsonl", "a", encoding="utf-8") as f:
|
| 192 |
+
f.write(json.dumps(feedback.model_dump()) + "\n")
|
| 193 |
+
return {"status": "ok"}
|
| 194 |
|
| 195 |
@app.post(
|
| 196 |
"/query",
|
src/rag/llm_client.py
CHANGED
|
@@ -1,103 +1,141 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Groq API client for LLM inference.
|
| 3 |
-
|
| 4 |
-
WHY GROQ:
|
| 5 |
-
- Free tier: 14,400 requests/day with Llama3
|
| 6 |
-
- Speed: ~500 tokens/second (vs 10 tokens/second local CPU)
|
| 7 |
-
- No GPU needed on our machine
|
| 8 |
-
- Production-quality latency for demos
|
| 9 |
-
|
| 10 |
-
WHY LLAMA3-8B:
|
| 11 |
-
- Free on Groq
|
| 12 |
-
- 8B parameters: strong reasoning for research QA
|
| 13 |
-
- 8192 token context window: fits our 5 retrieved chunks
|
| 14 |
-
- Fast: ~1-2 seconds for a full response
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
import os
|
|
|
|
|
|
|
| 18 |
from groq import Groq
|
| 19 |
from src.utils.logger import get_logger
|
| 20 |
from config.settings import (
|
| 21 |
GROQ_API_KEY,
|
| 22 |
-
|
| 23 |
LLM_TEMPERATURE,
|
| 24 |
LLM_MAX_TOKENS,
|
| 25 |
)
|
| 26 |
|
| 27 |
logger = get_logger(__name__)
|
| 28 |
|
| 29 |
-
|
| 30 |
-
class LLMClient:
|
| 31 |
"""
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
Designed as a simple interface so we can swap
|
| 35 |
-
to any other LLM provider (OpenAI, Anthropic, local)
|
| 36 |
-
by changing only this file.
|
| 37 |
"""
|
| 38 |
|
| 39 |
-
|
| 40 |
def __init__(self):
|
| 41 |
-
if
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
self.
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
def generate(
|
| 52 |
self,
|
| 53 |
system_prompt: str,
|
| 54 |
-
user_prompt:
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
"""
|
| 59 |
-
Generate
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
user_prompt: The actual question + context
|
| 64 |
-
temperature: 0.0 = deterministic, 1.0 = creative
|
| 65 |
-
We use 0.1 for factual research QA
|
| 66 |
-
max_tokens: Maximum response length
|
| 67 |
-
|
| 68 |
-
Returns:
|
| 69 |
-
Generated text string
|
| 70 |
-
|
| 71 |
-
GROQ API STRUCTURE:
|
| 72 |
-
Uses OpenAI-compatible chat format:
|
| 73 |
-
[{"role": "system", "content": "..."},
|
| 74 |
-
{"role": "user", "content": "..."}]
|
| 75 |
"""
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
f"
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
return answer
|
| 100 |
-
|
| 101 |
-
except Exception as e:
|
| 102 |
-
logger.error(f"LLM generation failed: {e}")
|
| 103 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
import json
|
| 3 |
+
import requests
|
| 4 |
from groq import Groq
|
| 5 |
from src.utils.logger import get_logger
|
| 6 |
from config.settings import (
|
| 7 |
GROQ_API_KEY,
|
| 8 |
+
HF_API_KEY,
|
| 9 |
LLM_TEMPERATURE,
|
| 10 |
LLM_MAX_TOKENS,
|
| 11 |
)
|
| 12 |
|
| 13 |
logger = get_logger(__name__)
|
| 14 |
|
| 15 |
+
class MultiModelClient:
|
|
|
|
| 16 |
"""
|
| 17 |
+
Multi-model LLM client with Qwen primary and Groq backup.
|
| 18 |
+
Supports code routing based on keywords.
|
|
|
|
|
|
|
|
|
|
| 19 |
"""
|
| 20 |
|
|
|
|
| 21 |
def __init__(self):
|
| 22 |
+
if GROQ_API_KEY:
|
| 23 |
+
self.groq_client = Groq(api_key=GROQ_API_KEY)
|
| 24 |
+
else:
|
| 25 |
+
self.groq_client = None
|
| 26 |
+
|
| 27 |
+
self.hf_api_key = HF_API_KEY
|
| 28 |
+
|
| 29 |
+
self.primary_model = "Qwen/Qwen2.5-72B-Instruct"
|
| 30 |
+
self.secondary_model = "llama-3.3-70b-versatile"
|
| 31 |
+
self.code_model = "Qwen/Qwen2.5-Coder-7B-Instruct"
|
| 32 |
+
|
| 33 |
+
self.code_keywords = ["code", "implement", "function", "class", "python", "algorithm", "write a", "script"]
|
| 34 |
+
|
| 35 |
+
def get_model_for_query(self, question: str):
|
| 36 |
+
q_lower = question.lower()
|
| 37 |
+
if any(kw in q_lower for kw in self.code_keywords):
|
| 38 |
+
return [self.code_model, self.primary_model, self.secondary_model]
|
| 39 |
+
return [self.primary_model, self.secondary_model]
|
| 40 |
+
|
| 41 |
+
def _call_hf(self, model_id, messages, temperature, max_tokens, stream=False):
|
| 42 |
+
if not self.hf_api_key:
|
| 43 |
+
raise ValueError("HF_API_KEY not configured")
|
| 44 |
+
|
| 45 |
+
url = f"https://api-inference.huggingface.co/models/{model_id}/v1/chat/completions"
|
| 46 |
+
headers = {
|
| 47 |
+
"Authorization": f"Bearer {self.hf_api_key}",
|
| 48 |
+
"Content-Type": "application/json"
|
| 49 |
+
}
|
| 50 |
+
payload = {
|
| 51 |
+
"model": model_id,
|
| 52 |
+
"messages": messages,
|
| 53 |
+
"max_tokens": max_tokens,
|
| 54 |
+
"temperature": temperature,
|
| 55 |
+
"stream": stream
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
response = requests.post(url, headers=headers, json=payload, stream=stream)
|
| 59 |
+
if response.status_code == 429:
|
| 60 |
+
raise Exception("Rate limit (HTTP 429)")
|
| 61 |
+
if not response.ok:
|
| 62 |
+
raise Exception(f"HF Error: {response.text}")
|
| 63 |
+
|
| 64 |
+
if stream:
|
| 65 |
+
def generator():
|
| 66 |
+
for line in response.iter_lines():
|
| 67 |
+
if line:
|
| 68 |
+
line = line.decode('utf-8')
|
| 69 |
+
if line.startswith("data: "):
|
| 70 |
+
data_str = line[6:]
|
| 71 |
+
if data_str.strip() == "[DONE]":
|
| 72 |
+
break
|
| 73 |
+
try:
|
| 74 |
+
data = json.loads(data_str)
|
| 75 |
+
token = data["choices"][0]["delta"].get("content", "")
|
| 76 |
+
if token:
|
| 77 |
+
yield token
|
| 78 |
+
except:
|
| 79 |
+
pass
|
| 80 |
+
return generator()
|
| 81 |
+
else:
|
| 82 |
+
return response.json()["choices"][0]["message"]["content"]
|
| 83 |
+
|
| 84 |
+
def _call_groq(self, model_id, messages, temperature, max_tokens, stream=False):
|
| 85 |
+
if not self.groq_client:
|
| 86 |
+
raise ValueError("GROQ_API_KEY not configured")
|
| 87 |
+
|
| 88 |
+
response = self.groq_client.chat.completions.create(
|
| 89 |
+
model=model_id,
|
| 90 |
+
messages=messages,
|
| 91 |
+
temperature=temperature,
|
| 92 |
+
max_tokens=max_tokens,
|
| 93 |
+
stream=stream
|
| 94 |
+
)
|
| 95 |
+
if stream:
|
| 96 |
+
def generator():
|
| 97 |
+
for chunk in response:
|
| 98 |
+
token = chunk.choices[0].delta.content
|
| 99 |
+
if token:
|
| 100 |
+
yield token
|
| 101 |
+
return generator()
|
| 102 |
+
else:
|
| 103 |
+
return response.choices[0].message.content
|
| 104 |
|
| 105 |
def generate(
|
| 106 |
self,
|
| 107 |
system_prompt: str,
|
| 108 |
+
user_prompt: str,
|
| 109 |
+
original_query: str = "",
|
| 110 |
+
temperature: float = LLM_TEMPERATURE,
|
| 111 |
+
max_tokens: int = LLM_MAX_TOKENS,
|
| 112 |
+
stream: bool = False
|
| 113 |
+
):
|
| 114 |
"""
|
| 115 |
+
Generate response trying models in priority order.
|
| 116 |
+
Returns a tuple of (result, model_used).
|
| 117 |
+
If stream=True, result is a generator.
|
| 118 |
+
Otherwise, result is a string.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
"""
|
| 120 |
+
models_to_try = self.get_model_for_query(original_query)
|
| 121 |
+
messages = [
|
| 122 |
+
{"role": "system", "content": system_prompt},
|
| 123 |
+
{"role": "user", "content": user_prompt}
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
for model in models_to_try:
|
| 127 |
+
try:
|
| 128 |
+
is_hf = "Qwen" in model
|
| 129 |
+
logger.info(f"Attempting model: {model}")
|
| 130 |
+
if is_hf:
|
| 131 |
+
out = self._call_hf(model, messages, temperature, max_tokens, stream)
|
| 132 |
+
else:
|
| 133 |
+
out = self._call_groq(model, messages, temperature, max_tokens, stream)
|
| 134 |
+
|
| 135 |
+
logger.info(f"Model {model} selected successfully.")
|
| 136 |
+
return out, model
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.warning(f"Model {model} failed: {e}")
|
| 139 |
+
continue
|
| 140 |
+
|
| 141 |
+
raise Exception("All models failed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/rag/pipeline.py
CHANGED
|
@@ -13,11 +13,12 @@ PIPELINE FLOW:
|
|
| 13 |
"""
|
| 14 |
|
| 15 |
import time
|
|
|
|
| 16 |
from dataclasses import dataclass, field
|
| 17 |
from typing import Optional
|
| 18 |
|
| 19 |
from src.retrieval.retrieval_pipeline import RetrievalPipeline
|
| 20 |
-
from src.rag.llm_client import
|
| 21 |
from src.rag.prompt_templates import (
|
| 22 |
SYSTEM_PROMPT,
|
| 23 |
build_rag_prompt,
|
|
@@ -31,32 +32,15 @@ logger = get_logger(__name__)
|
|
| 31 |
|
| 32 |
@dataclass
|
| 33 |
class RAGResponse:
|
| 34 |
-
"""
|
| 35 |
-
Structured response from the RAG pipeline.
|
| 36 |
-
|
| 37 |
-
WHY A DATACLASS INSTEAD OF A DICT:
|
| 38 |
-
Dicts can have any keys - you never know what's in them.
|
| 39 |
-
A dataclass defines the exact contract. The FastAPI layer
|
| 40 |
-
(Phase 11) and frontend (Phase 12) can rely on these
|
| 41 |
-
fields always being present.
|
| 42 |
-
"""
|
| 43 |
-
# The generated answer
|
| 44 |
answer: str
|
| 45 |
-
|
| 46 |
-
# Source papers used to generate the answer
|
| 47 |
citations: list[dict]
|
| 48 |
-
|
| 49 |
-
# Raw retrieved chunks (for debugging / evaluation)
|
| 50 |
retrieved_chunks: list[dict]
|
| 51 |
-
|
| 52 |
-
# Performance metadata
|
| 53 |
query: str
|
| 54 |
retrieval_time_ms: float
|
| 55 |
generation_time_ms: float
|
| 56 |
total_time_ms: float
|
| 57 |
-
|
| 58 |
-
# Whether retrieval found retrieval content
|
| 59 |
has_context: bool
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def to_dict(self) -> dict:
|
|
@@ -69,29 +53,14 @@ class RAGResponse:
|
|
| 69 |
"total_time_ms": round(self.total_time_ms, 1),
|
| 70 |
"has_context": self.has_context,
|
| 71 |
"chunks_used": len(self.retrieved_chunks),
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
class RAGPipeline:
|
| 78 |
-
"""
|
| 79 |
-
End-to-end RAG pipeline: query -> retrieve -> generate -> respond.
|
| 80 |
-
|
| 81 |
-
Usage:
|
| 82 |
-
pipeline = RAGPipeline()
|
| 83 |
-
response = pipeline.query("How does LoRA reduce training parameters?")
|
| 84 |
-
print(response.answer)
|
| 85 |
-
for cite in response.citations:
|
| 86 |
-
print(cite["title"], cite["arxiv_url"])
|
| 87 |
-
"""
|
| 88 |
-
|
| 89 |
def __init__(self):
|
| 90 |
logger.info("Initializing RAGPipeline...")
|
| 91 |
-
|
| 92 |
self.retriever = RetrievalPipeline()
|
| 93 |
-
self.llm =
|
| 94 |
-
|
| 95 |
logger.info("RAGPipeline ready")
|
| 96 |
|
| 97 |
def query(
|
|
@@ -101,26 +70,11 @@ class RAGPipeline:
|
|
| 101 |
filter_category: Optional[str] = None,
|
| 102 |
filter_year_gte: Optional[int] = None,
|
| 103 |
) -> RAGResponse:
|
| 104 |
-
"""
|
| 105 |
-
Process a user question through the full RAG pipeline.
|
| 106 |
-
|
| 107 |
-
Args:
|
| 108 |
-
question: User's natural language question
|
| 109 |
-
top_k: Number of chunks to retrieve
|
| 110 |
-
filter_category: Optional ArXiv category filter
|
| 111 |
-
filter_year_gte: Optional year filter
|
| 112 |
-
|
| 113 |
-
Returns:
|
| 114 |
-
RAGResponse with answer, citations, and timing metadata
|
| 115 |
-
"""
|
| 116 |
question = question.strip()
|
| 117 |
-
|
| 118 |
if not question:
|
| 119 |
raise ValueError("Question cannot be empty")
|
| 120 |
|
| 121 |
total_start = time.time()
|
| 122 |
-
|
| 123 |
-
# ------------ Stage 1: Retrieval ------------
|
| 124 |
retrieval_start = time.time()
|
| 125 |
|
| 126 |
chunks = self.retriever.retrieve(
|
|
@@ -129,20 +83,12 @@ class RAGPipeline:
|
|
| 129 |
filter_category = filter_category,
|
| 130 |
filter_year_gte = filter_year_gte,
|
| 131 |
)
|
| 132 |
-
|
| 133 |
retrieval_ms = (time.time() - retrieval_start) * 1000
|
| 134 |
-
|
| 135 |
-
logger.info(
|
| 136 |
-
f"Retrieved: {len(chunks)} chunks in {retrieval_ms:.0f}ms"
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
has_context = len(chunks) > 0
|
| 140 |
|
| 141 |
-
# ------------ Stage 2: Prompt Construction ------------
|
| 142 |
if has_context:
|
| 143 |
user_prompt = build_rag_prompt(question, chunks)
|
| 144 |
else:
|
| 145 |
-
# Fallback prompt when no relevant context found
|
| 146 |
user_prompt = (
|
| 147 |
f"The user asked: {question}\n\n"
|
| 148 |
f"No relevant research papers were found in the database. "
|
|
@@ -150,23 +96,16 @@ class RAGPipeline:
|
|
| 150 |
f"or broadening their query."
|
| 151 |
)
|
| 152 |
|
| 153 |
-
# ------------ Stage 3: LLM Generation ------------
|
| 154 |
generation_start = time.time()
|
| 155 |
-
|
| 156 |
-
answer = self.llm.generate(
|
| 157 |
system_prompt = SYSTEM_PROMPT,
|
| 158 |
user_prompt = user_prompt,
|
|
|
|
|
|
|
| 159 |
)
|
| 160 |
|
| 161 |
generation_ms = (time.time() - generation_start) * 1000
|
| 162 |
total_ms = (time.time() - total_start) * 1000
|
| 163 |
-
|
| 164 |
-
logger.info(
|
| 165 |
-
f"Generated answer in {generation_ms:.0f}ms | "
|
| 166 |
-
f"Total: {total_ms:.0f}ms"
|
| 167 |
-
)
|
| 168 |
-
|
| 169 |
-
# ------------ Stage 4: Build Citations ------------
|
| 170 |
citations = build_citation_list(chunks)
|
| 171 |
|
| 172 |
return RAGResponse(
|
|
@@ -178,4 +117,65 @@ class RAGPipeline:
|
|
| 178 |
generation_time_ms = generation_ms,
|
| 179 |
total_time_ms = total_ms,
|
| 180 |
has_context = has_context,
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"""
|
| 14 |
|
| 15 |
import time
|
| 16 |
+
import json
|
| 17 |
from dataclasses import dataclass, field
|
| 18 |
from typing import Optional
|
| 19 |
|
| 20 |
from src.retrieval.retrieval_pipeline import RetrievalPipeline
|
| 21 |
+
from src.rag.llm_client import MultiModelClient
|
| 22 |
from src.rag.prompt_templates import (
|
| 23 |
SYSTEM_PROMPT,
|
| 24 |
build_rag_prompt,
|
|
|
|
| 32 |
|
| 33 |
@dataclass
|
| 34 |
class RAGResponse:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
answer: str
|
|
|
|
|
|
|
| 36 |
citations: list[dict]
|
|
|
|
|
|
|
| 37 |
retrieved_chunks: list[dict]
|
|
|
|
|
|
|
| 38 |
query: str
|
| 39 |
retrieval_time_ms: float
|
| 40 |
generation_time_ms: float
|
| 41 |
total_time_ms: float
|
|
|
|
|
|
|
| 42 |
has_context: bool
|
| 43 |
+
model_used: str = ""
|
| 44 |
|
| 45 |
|
| 46 |
def to_dict(self) -> dict:
|
|
|
|
| 53 |
"total_time_ms": round(self.total_time_ms, 1),
|
| 54 |
"has_context": self.has_context,
|
| 55 |
"chunks_used": len(self.retrieved_chunks),
|
| 56 |
+
"model_used": self.model_used,
|
| 57 |
}
|
| 58 |
|
|
|
|
|
|
|
|
|
|
| 59 |
class RAGPipeline:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
def __init__(self):
|
| 61 |
logger.info("Initializing RAGPipeline...")
|
|
|
|
| 62 |
self.retriever = RetrievalPipeline()
|
| 63 |
+
self.llm = MultiModelClient()
|
|
|
|
| 64 |
logger.info("RAGPipeline ready")
|
| 65 |
|
| 66 |
def query(
|
|
|
|
| 70 |
filter_category: Optional[str] = None,
|
| 71 |
filter_year_gte: Optional[int] = None,
|
| 72 |
) -> RAGResponse:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
question = question.strip()
|
|
|
|
| 74 |
if not question:
|
| 75 |
raise ValueError("Question cannot be empty")
|
| 76 |
|
| 77 |
total_start = time.time()
|
|
|
|
|
|
|
| 78 |
retrieval_start = time.time()
|
| 79 |
|
| 80 |
chunks = self.retriever.retrieve(
|
|
|
|
| 83 |
filter_category = filter_category,
|
| 84 |
filter_year_gte = filter_year_gte,
|
| 85 |
)
|
|
|
|
| 86 |
retrieval_ms = (time.time() - retrieval_start) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
has_context = len(chunks) > 0
|
| 88 |
|
|
|
|
| 89 |
if has_context:
|
| 90 |
user_prompt = build_rag_prompt(question, chunks)
|
| 91 |
else:
|
|
|
|
| 92 |
user_prompt = (
|
| 93 |
f"The user asked: {question}\n\n"
|
| 94 |
f"No relevant research papers were found in the database. "
|
|
|
|
| 96 |
f"or broadening their query."
|
| 97 |
)
|
| 98 |
|
|
|
|
| 99 |
generation_start = time.time()
|
| 100 |
+
answer, model_used = self.llm.generate(
|
|
|
|
| 101 |
system_prompt = SYSTEM_PROMPT,
|
| 102 |
user_prompt = user_prompt,
|
| 103 |
+
original_query = question,
|
| 104 |
+
stream=False
|
| 105 |
)
|
| 106 |
|
| 107 |
generation_ms = (time.time() - generation_start) * 1000
|
| 108 |
total_ms = (time.time() - total_start) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
citations = build_citation_list(chunks)
|
| 110 |
|
| 111 |
return RAGResponse(
|
|
|
|
| 117 |
generation_time_ms = generation_ms,
|
| 118 |
total_time_ms = total_ms,
|
| 119 |
has_context = has_context,
|
| 120 |
+
model_used = model_used
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
def stream_query(
|
| 124 |
+
self,
|
| 125 |
+
question: str,
|
| 126 |
+
top_k: int = TOP_K_RERANK,
|
| 127 |
+
filter_category: Optional[str] = None,
|
| 128 |
+
filter_year_gte: Optional[int] = None,
|
| 129 |
+
):
|
| 130 |
+
question = question.strip()
|
| 131 |
+
if not question:
|
| 132 |
+
raise ValueError("Question cannot be empty")
|
| 133 |
+
|
| 134 |
+
total_start = time.time()
|
| 135 |
+
retrieval_start = time.time()
|
| 136 |
+
chunks = self.retriever.retrieve(
|
| 137 |
+
query = question,
|
| 138 |
+
top_k_final = top_k,
|
| 139 |
+
filter_category = filter_category,
|
| 140 |
+
filter_year_gte = filter_year_gte,
|
| 141 |
+
)
|
| 142 |
+
retrieval_ms = (time.time() - retrieval_start) * 1000
|
| 143 |
+
has_context = len(chunks) > 0
|
| 144 |
+
|
| 145 |
+
if has_context:
|
| 146 |
+
user_prompt = build_rag_prompt(question, chunks)
|
| 147 |
+
else:
|
| 148 |
+
user_prompt = (
|
| 149 |
+
f"The user asked: {question}\n\n"
|
| 150 |
+
f"No relevant research papers were found in the database. "
|
| 151 |
+
f"Politely inform the user and suggest they try rephrasing "
|
| 152 |
+
f"or broadening their query."
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
generation_start = time.time()
|
| 156 |
+
generator, model_used = self.llm.generate(
|
| 157 |
+
system_prompt = SYSTEM_PROMPT,
|
| 158 |
+
user_prompt = user_prompt,
|
| 159 |
+
original_query = question,
|
| 160 |
+
stream=True
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
for token in generator:
|
| 164 |
+
yield f"data: {json.dumps({'token': token})}\n\n"
|
| 165 |
+
|
| 166 |
+
generation_ms = (time.time() - generation_start) * 1000
|
| 167 |
+
total_ms = (time.time() - total_start) * 1000
|
| 168 |
+
citations = build_citation_list(chunks)
|
| 169 |
+
|
| 170 |
+
metadata = {
|
| 171 |
+
"done": True,
|
| 172 |
+
"citations": citations,
|
| 173 |
+
"model_used": model_used,
|
| 174 |
+
"timing": {
|
| 175 |
+
"retrieval_time_ms": round(retrieval_ms, 1),
|
| 176 |
+
"generation_time_ms": round(generation_ms, 1),
|
| 177 |
+
"total_time_ms": round(total_ms, 1),
|
| 178 |
+
"chunks_used": len(chunks)
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
yield f"data: {json.dumps(metadata)}\n\n"
|