}
+// | { type: "RESET_RESULTS" };
+
+const initialState = {
+ mode: "chat",
+ files: [],
+ connectorsOpen: false,
+ selectedConnectors: new Set(),
+ toasts: [],
+ deepProgress: { running: false, stage: "idle", log: [] },
+ results: {},
+};
+
+const StoreContext = React.createContext(null);
+
+function reducer(state, action) {
+ switch (action.type) {
+ case "SET_MODE":
+ return { ...state, mode: action.mode };
+ case "ADD_FILES": {
+ const next = [...state.files];
+ action.files.forEach((f) => {
+ if (!next.some((x) => x.name === f.name && x.size === f.size && x.lastModified === f.lastModified)) {
+ next.push(f);
+ }
+ });
+ return { ...state, files: next };
+ }
+ case "REMOVE_FILE": {
+ const next = [...state.files];
+ next.splice(action.index, 1);
+ return { ...state, files: next };
+ }
+ case "OPEN_CONNECTORS":
+ return { ...state, connectorsOpen: action.open };
+ case "TOGGLE_CONNECTOR": {
+ const s = new Set(state.selectedConnectors);
+ if (s.has(action.id)) s.delete(action.id); else s.add(action.id);
+ return { ...state, selectedConnectors: s };
+ }
+ case "ADD_TOAST":
+ return { ...state, toasts: [...state.toasts, action.toast] };
+ case "REMOVE_TOAST":
+ return { ...state, toasts: state.toasts.filter((t) => t.id !== action.id) };
+ case "DEEP_START":
+ return { ...state, deepProgress: { running: true, stage: "planning", startedAt: Date.now(), log: [] } };
+ case "DEEP_SET_STAGE": {
+ const log = [...state.deepProgress.log];
+ if (action.log) log.push(action.log);
+ return { ...state, deepProgress: { ...state.deepProgress, stage: action.stage, log } };
+ }
+ case "DEEP_STOP":
+ return { ...state, deepProgress: { running: false, stage: "idle", log: [] } };
+ case "SET_RESULTS":
+ return { ...state, results: { ...state.results, ...action.results } };
+ case "RESET_RESULTS":
+ return { ...state, results: {} };
+ default:
+ return state;
+ }
+}
+
+function StoreProvider({ children }) {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ return {children};
+}
+
+function useStore() {
+ const ctx = React.useContext(StoreContext);
+ if (!ctx) throw new Error("useStore must be used within StoreProvider");
+ return ctx;
+}
+
+// ------------------------------
+// Utility helpers
+// ------------------------------
+
+function classNames(...xs) {
+ return xs.filter(Boolean).join(" ");
+}
+
+function uid(prefix = "id") {
+ return `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
+}
+
+const API_KEY = import.meta.env.VITE_API_KEY || "";
+
+// API helpers
+async function deepResearch(query, onStage) {
+ onStage("planning", "Submitting query to agent…");
+ const res = await api.post("/deep-research", { query });
+ onStage("finalizing", "Deep research complete");
+ return res;
+}
+
+async function imageGen(prompt, files) {
+ const form = new FormData();
+ form.append("prompt", prompt);
+ files.forEach((f) => form.append("files", f));
+ const headers = {};
+ if (API_KEY) headers["x-api-key"] = API_KEY;
+ const url = `${API_BASE_URL.replace(/\/$/, "")}/image-gen`;
+ const res = await fetch(url, { method: "POST", headers, body: form });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data?.message || "Image generation failed");
+ }
+ return data.images || [];
+}
+
+// ------------------------------
+// UI Components
+// ------------------------------
+
+function Card({ children, className }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function SectionTitle({ title, subtitle }) {
+ return (
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+ );
+}
+
+function Toasts() {
+ const { state, dispatch } = useStore();
+ useEffect(() => {
+ if (!state.toasts.length) return;
+ const timers = state.toasts.map((t) => setTimeout(() => dispatch({ type: "REMOVE_TOAST", id: t.id }), 4000));
+ return () => timers.forEach(clearTimeout);
+ }, [state.toasts, dispatch]);
+ return (
+
+ {state.toasts.map((t) => (
+
+
{t.title}
+ {t.desc &&
{t.desc}
}
+
+ ))}
+
+ );
+}
+
+function ModeSelector({ mode, onChange }) {
+ const groupId = useId();
+ const options = [
+ { id: "chat", label: "Chat" },
+ { id: "deep", label: "Deep research", desc: "Web + multi-step agent" },
+ { id: "image", label: "Create image" },
+ { id: "connectors", label: "Connectors" },
+ ];
+
+ function handleKeyDown(e) {
+ const idx = options.findIndex((o) => o.id === mode);
+ const next = (delta) => options[(idx + delta + options.length) % options.length].id;
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
+ onChange(next(+1));
+ e.preventDefault();
+ }
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
+ onChange(next(-1));
+ e.preventDefault();
+ }
+ }
+
+ return (
+
+
+ Response mode
+
+ {options.map((opt) => (
+
+ ))}
+
+ );
+}
+
+const PLACEHOLDER = {
+ chat: "Ask anything…",
+ deep: "Ask a complex question for deep research (will consult the web)…",
+ image: "Describe the image you want to create…",
+ connectors: "Ask using selected sources (e.g., Drive, GitHub)…",
+};
+
+function FileChips() {
+ const { state, dispatch } = useStore();
+ if (!state.files.length) return null;
+ return (
+
+ {state.files.map((f, i) => (
+
+
+ {f.name}
+
+
+
+ ))}
+
+ );
+}
+
+function ActionTray() {
+ const { state, dispatch } = useStore();
+ const inputRef = useRef(null);
+ const accept = state.mode === "image" ? "image/*" : ".pdf,.csv,.xlsx,image/*,.txt";
+
+ function onFilesSelected(fs) {
+ if (!fs.length) return;
+ const maxMB = 25;
+ const bad = fs.find((f) => f.size > maxMB * 1024 * 1024);
+ if (bad) {
+ dispatch({ type: "ADD_TOAST", toast: { id: uid("toast"), title: "File too large", desc: `${bad.name} exceeds ${maxMB}MB`, kind: "error" } });
+ return;
+ }
+ dispatch({ type: "ADD_FILES", files: fs });
+ dispatch({ type: "ADD_TOAST", toast: { id: uid("toast"), title: `${fs.length} file(s) added`, kind: "success" } });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function MoreMenu() {
+ const [open, setOpen] = useState(false);
+ const menuRef = useRef(null);
+ useEffect(() => {
+ function onDoc(e) {
+ if (!menuRef.current) return;
+ if (!menuRef.current.contains(e.target)) setOpen(false);
+ }
+ document.addEventListener("click", onDoc);
+ return () => document.removeEventListener("click", onDoc);
+ }, []);
+ return (
+
+
+ {open && (
+
+ {[
+ { id: "voice", label: "Start voice / record" },
+ { id: "templates", label: "Prompt templates" },
+ { id: "tools", label: "Tools gallery" },
+ ].map((it) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+function ConnectorsModal() {
+ const { state, dispatch } = useStore();
+ const items = [
+ { id: "google-drive", label: "Google Drive", desc: "Pick files/folders to search" },
+ { id: "github", label: "GitHub", desc: "Repos, issues, PRs" },
+ { id: "sharepoint", label: "SharePoint", desc: "Sites & document libraries" },
+ { id: "box", label: "Box", desc: "Cloud files & folders" },
+ ];
+ if (!state.connectorsOpen) return null;
+ return (
+
+
+
+
+
+
+
+ {items.map((it) => {
+ const checked = state.selectedConnectors.has(it.id);
+ return (
+
+ );
+ })}
+
+
+
+ Selected: {state.selectedConnectors.size || 0}
+
+
+
+
+
+ );
+}
+
+function DeepProgressBanner() {
+ const { state, dispatch } = useStore();
+ if (!state.deepProgress.running) return null;
+ const stages = [
+ { id: "planning", label: "Planning" },
+ { id: "searching", label: "Searching" },
+ { id: "synthesizing", label: "Synthesizing" },
+ { id: "finalizing", label: "Finalizing" },
+ ];
+ return (
+
+
+
+
+
Deep research in progress…
+
+
+
+
+ {stages.map((s) => (
+
+ ))}
+
+ {state.deepProgress.log.length > 0 && (
+
+ {state.deepProgress.log.slice(-3).map((l, i) => (
+
• {l}
+ ))}
+
+ )}
+
+ );
+}
+
+function ResultsPanel() {
+ const { state } = useStore();
+ if (!state.results.text && !state.results.images?.length) return null;
+ return (
+
+
+ {state.results.text && (
+
+ {state.results.text}
+
+ )}
+ {state.results.citations && state.results.citations.length > 0 && (
+
+
Citations
+
+ {state.results.citations.map((c, i) => (
+ -
+
+ {c.title}
+
+
+ ))}
+
+
+ )}
+ {state.results.images && state.results.images.length > 0 && (
+
+ {state.results.images.map((src, i) => (
+

+ ))}
+
+ )}
+
+ );
+}
+
+function ChatComposer() {
+ const { state, dispatch } = useStore();
+ const [value, setValue] = useState("");
+ const submittingRef = useRef(false);
+
+ async function handleSubmit() {
+ if (submittingRef.current) return;
+ submittingRef.current = true;
+ dispatch({ type: "RESET_RESULTS" });
+
+ const q = value.trim();
+ if (!q) {
+ dispatch({ type: "ADD_TOAST", toast: { id: uid("toast"), title: "Type something to send", kind: "info" } });
+ submittingRef.current = false;
+ return;
+ }
+
+ try {
+ if (state.mode === "chat") {
+ dispatch({ type: "SET_RESULTS", results: { text: `You said: ${q}\n\n(Connect your backend to replace this mock.)` } });
+ } else if (state.mode === "deep") {
+ dispatch({ type: "DEEP_START" });
+ const res = await deepResearch(q, (stage, log) => {
+ dispatch({ type: "DEEP_SET_STAGE", stage, log });
+ });
+ dispatch({ type: "SET_RESULTS", results: { text: res.text, citations: res.citations } });
+ dispatch({ type: "DEEP_STOP" });
+ } else if (state.mode === "image") {
+ const images = await imageGen(q, state.files);
+ dispatch({ type: "SET_RESULTS", results: { images } });
+ dispatch({ type: "ADD_TOAST", toast: { id: uid("toast"), title: "Images ready", kind: "success" } });
+ } else if (state.mode === "connectors") {
+ if (state.selectedConnectors.size === 0) {
+ dispatch({ type: "ADD_TOAST", toast: { id: uid("toast"), title: "Select sources first", desc: "Open Connectors to choose data.", kind: "error" } });
+ } else {
+ const res = await api.post("/connectors/query", {
+ query: q,
+ connectors: Array.from(state.selectedConnectors),
+ });
+ dispatch({ type: "SET_RESULTS", results: { text: res.text, citations: res.citations } });
+ }
+ }
+ } finally {
+ submittingRef.current = false;
+ }
+ }
+
+ return (
+
+
+
+
+ dispatch({ type: "SET_MODE", mode: m })} />
+
+
+
+
+
+
+
+ );
+}
+
+function ChatWorkbench() {
+ return (
+
+ );
+}
+
+export default function DemoApp() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/chat-ui/src/components/ProfileMenu.jsx b/frontend/chat-ui/src/components/ProfileMenu.jsx
index 34636f2..72ada21 100644
--- a/frontend/chat-ui/src/components/ProfileMenu.jsx
+++ b/frontend/chat-ui/src/components/ProfileMenu.jsx
@@ -10,6 +10,7 @@ export default function ProfileMenu() {
return (