Commit 06918c59 authored by Mahmoud Aglan's avatar Mahmoud Aglan

again

parent 25a6e3a4
This diff is collapsed.
...@@ -256,7 +256,7 @@ tar cf "$TARBALL" \ ...@@ -256,7 +256,7 @@ tar cf "$TARBALL" \
--exclude='.env.local' \ --exclude='.env.local' \
--exclude='.idea' \ --exclude='.idea' \
--exclude='.vscode' \ --exclude='.vscode' \
--exclude='main.py' \ --exclude='./main.py' \
--exclude='create-project.ps1' \ --exclude='create-project.ps1' \
--exclude='*.sh' \ --exclude='*.sh' \
. .
......
import React from "react"; import React, { useEffect, useState } from "react";
import { Routes, Route, Navigate } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import { useApp } from "./store"; import { useApp } from "./store";
import { getMe } from "./api";
import LoginPage from "./pages/LoginPage"; import LoginPage from "./pages/LoginPage";
import ChatPage from "./pages/ChatPage"; import ChatPage from "./pages/ChatPage";
import AdminPage from "./pages/AdminPage"; import AdminPage from "./pages/AdminPage";
import { Flame } from "lucide-react";
export default function App() { export default function App() {
const { state } = useApp(); const { state, dispatch } = useApp();
const loggedIn = !!state.token; const [authChecked, setAuthChecked] = useState(!state.token);
if (!loggedIn) { useEffect(() => {
if (!state.token) {
setAuthChecked(true);
return;
}
if (state.user) {
setAuthChecked(true);
return;
}
(async () => {
try {
const user = await getMe(state.token);
dispatch({ type: "SET_USER", user });
} catch {
dispatch({ type: "LOGOUT" });
} finally {
setAuthChecked(true);
}
})();
}, [state.token, state.user, dispatch]);
if (!authChecked) {
return (
<div className="h-full flex items-center justify-center bg-anton-bg">
<div className="flex flex-col items-center gap-4 animate-fade-in">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
<Flame size={32} className="text-white animate-pulse" />
</div>
<p className="text-anton-muted text-sm">Loading...</p>
</div>
</div>
);
}
if (!state.token) {
return <LoginPage />; return <LoginPage />;
} }
......
...@@ -38,10 +38,9 @@ export default function Sidebar({ onRefresh }) { ...@@ -38,10 +38,9 @@ export default function Sidebar({ onRefresh }) {
async function handleDelete(id) { async function handleDelete(id) {
try { try {
// Abort any active stream for this chat before deleting
streamManager.abortStream(id); streamManager.abortStream(id);
await deleteChat(state.token, id); await deleteChat(state.token, id);
dispatch({ type: "REMOVE_CHAT", chatId: id }); dispatch({ type: "DELETE_CHAT", chatId: id });
} catch { /* */ } } catch { /* */ }
} }
...@@ -89,7 +88,7 @@ export default function Sidebar({ onRefresh }) { ...@@ -89,7 +88,7 @@ export default function Sidebar({ onRefresh }) {
if (errors.length > 0) { if (errors.length > 0) {
alert( alert(
`Uploaded ${result.files.length - errors.length} of ${result.files.length} files.\n\nErrors:\n` + `Uploaded ${result.files.length - errors.length} of ${result.files.length} files.\n\nErrors:\n` +
errors.map((e) => `• ${e.filename}: ${e.error}`).join("\n") errors.map((e) => `• ${e.filename}: ${e.error}`).join("\n")
); );
} }
loadKbs(); loadKbs();
...@@ -106,7 +105,6 @@ export default function Sidebar({ onRefresh }) { ...@@ -106,7 +105,6 @@ export default function Sidebar({ onRefresh }) {
if (t === "knowledge" && !kbLoaded) loadKbs(); if (t === "knowledge" && !kbLoaded) loadKbs();
} }
// ── Collapsed sidebar ──
if (!open) { if (!open) {
return ( return (
<div className="w-12 bg-anton-surface border-r border-anton-border flex flex-col items-center py-3 gap-3 shrink-0"> <div className="w-12 bg-anton-surface border-r border-anton-border flex flex-col items-center py-3 gap-3 shrink-0">
...@@ -122,7 +120,6 @@ export default function Sidebar({ onRefresh }) { ...@@ -122,7 +120,6 @@ export default function Sidebar({ onRefresh }) {
> >
<Plus size={18} /> <Plus size={18} />
</button> </button>
{/* Streaming count badge when sidebar is collapsed */}
{streamingCount > 0 && ( {streamingCount > 0 && (
<div className="w-6 h-6 rounded-full bg-anton-accent/20 flex items-center justify-center"> <div className="w-6 h-6 rounded-full bg-anton-accent/20 flex items-center justify-center">
<span className="text-[10px] text-anton-accent font-bold animate-pulse"> <span className="text-[10px] text-anton-accent font-bold animate-pulse">
...@@ -134,10 +131,8 @@ export default function Sidebar({ onRefresh }) { ...@@ -134,10 +131,8 @@ export default function Sidebar({ onRefresh }) {
); );
} }
// ── Full sidebar ──
return ( return (
<div className="w-72 bg-anton-surface border-r border-anton-border flex flex-col shrink-0"> <div className="w-72 bg-anton-surface border-r border-anton-border flex flex-col shrink-0">
{/* Header */}
<div className="p-3 border-b border-anton-border flex items-center justify-between"> <div className="p-3 border-b border-anton-border flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Flame size={20} className="text-anton-accent" /> <Flame size={20} className="text-anton-accent" />
...@@ -156,7 +151,6 @@ export default function Sidebar({ onRefresh }) { ...@@ -156,7 +151,6 @@ export default function Sidebar({ onRefresh }) {
</button> </button>
</div> </div>
{/* Tabs */}
<div className="flex border-b border-anton-border"> <div className="flex border-b border-anton-border">
{[ {[
{ key: "chats", label: "Chats", icon: MessageSquare }, { key: "chats", label: "Chats", icon: MessageSquare },
...@@ -165,11 +159,10 @@ export default function Sidebar({ onRefresh }) { ...@@ -165,11 +159,10 @@ export default function Sidebar({ onRefresh }) {
<button <button
key={t.key} key={t.key}
onClick={() => switchTab(t.key)} onClick={() => switchTab(t.key)}
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-xs font-medium transition ${ className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-xs font-medium transition ${tab === t.key
tab === t.key ? "text-anton-accent border-b-2 border-anton-accent"
? "text-anton-accent border-b-2 border-anton-accent" : "text-anton-muted hover:text-white"
: "text-anton-muted hover:text-white" }`}
}`}
> >
<t.icon size={13} /> <t.icon size={13} />
{t.label} {t.label}
...@@ -177,7 +170,6 @@ export default function Sidebar({ onRefresh }) { ...@@ -177,7 +170,6 @@ export default function Sidebar({ onRefresh }) {
))} ))}
</div> </div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-2 space-y-1"> <div className="flex-1 overflow-y-auto p-2 space-y-1">
{tab === "chats" && ( {tab === "chats" && (
<> <>
...@@ -193,11 +185,10 @@ export default function Sidebar({ onRefresh }) { ...@@ -193,11 +185,10 @@ export default function Sidebar({ onRefresh }) {
return ( return (
<div <div
key={c.id} key={c.id}
className={`group flex items-center rounded-lg cursor-pointer transition ${ className={`group flex items-center rounded-lg cursor-pointer transition ${state.activeChatId === c.id
state.activeChatId === c.id ? "bg-anton-accent/10 text-anton-accent"
? "bg-anton-accent/10 text-anton-accent" : "text-anton-text hover:bg-anton-card"
: "text-anton-text hover:bg-anton-card" }`}
}`}
> >
{renamingId === c.id ? ( {renamingId === c.id ? (
<div className="flex items-center gap-1 flex-1 p-1"> <div className="flex items-center gap-1 flex-1 p-1">
...@@ -221,7 +212,6 @@ export default function Sidebar({ onRefresh }) { ...@@ -221,7 +212,6 @@ export default function Sidebar({ onRefresh }) {
onClick={() => dispatch({ type: "SET_ACTIVE_CHAT", chatId: c.id })} onClick={() => dispatch({ type: "SET_ACTIVE_CHAT", chatId: c.id })}
className="flex-1 flex items-center gap-2 text-left px-3 py-2 text-sm truncate min-w-0" className="flex-1 flex items-center gap-2 text-left px-3 py-2 text-sm truncate min-w-0"
> >
{/* Streaming indicator dot */}
{chatStreaming && ( {chatStreaming && (
<span className="w-2 h-2 bg-anton-accent rounded-full animate-pulse shrink-0" /> <span className="w-2 h-2 bg-anton-accent rounded-full animate-pulse shrink-0" />
)} )}
...@@ -298,9 +288,8 @@ export default function Sidebar({ onRefresh }) { ...@@ -298,9 +288,8 @@ export default function Sidebar({ onRefresh }) {
</div> </div>
</div> </div>
<label <label
className={`flex items-center gap-1.5 px-2 py-1.5 rounded border border-dashed border-anton-border text-xs text-anton-muted hover:text-anton-accent hover:border-anton-accent transition cursor-pointer ${ className={`flex items-center gap-1.5 px-2 py-1.5 rounded border border-dashed border-anton-border text-xs text-anton-muted hover:text-anton-accent hover:border-anton-accent transition cursor-pointer ${uploading ? "opacity-50 pointer-events-none" : ""
uploading ? "opacity-50 pointer-events-none" : "" }`}
}`}
> >
<Upload size={12} /> <Upload size={12} />
{uploading {uploading
...@@ -333,7 +322,6 @@ export default function Sidebar({ onRefresh }) { ...@@ -333,7 +322,6 @@ export default function Sidebar({ onRefresh }) {
)} )}
</div> </div>
{/* Footer */}
<div className="p-3 border-t border-anton-border space-y-2"> <div className="p-3 border-t border-anton-border space-y-2">
{state.user?.role === "superadmin" && ( {state.user?.role === "superadmin" && (
<button <button
......
...@@ -5,27 +5,35 @@ const AppContext = createContext(); ...@@ -5,27 +5,35 @@ const AppContext = createContext();
const initialState = { const initialState = {
token: localStorage.getItem("token") || null, token: localStorage.getItem("token") || null,
user: null, user: JSON.parse(localStorage.getItem("user") || "null"),
chats: [], chats: [],
activeChatId: null, activeChatId: null,
sidebarOpen: true, sidebarOpen: true,
chatMessages: {}, // chatId -> [messages] chatMessages: {},
activeStreams: {}, // chatId -> true (which chats are currently streaming) activeStreams: {},
}; };
function reducer(state, action) { function reducer(state, action) {
switch (action.type) { switch (action.type) {
case "LOGIN": {
localStorage.setItem("token", action.token);
localStorage.setItem("user", JSON.stringify(action.user));
return { ...state, token: action.token, user: action.user };
}
case "SET_TOKEN": case "SET_TOKEN":
if (action.token) localStorage.setItem("token", action.token); if (action.token) localStorage.setItem("token", action.token);
else localStorage.removeItem("token"); else localStorage.removeItem("token");
return { ...state, token: action.token }; return { ...state, token: action.token };
case "SET_USER": case "SET_USER":
localStorage.setItem("user", JSON.stringify(action.user));
return { ...state, user: action.user }; return { ...state, user: action.user };
case "LOGOUT": case "LOGOUT":
localStorage.removeItem("token"); localStorage.removeItem("token");
return { ...initialState, token: null }; localStorage.removeItem("user");
return { ...initialState, token: null, user: null };
case "SET_CHATS": case "SET_CHATS":
return { ...state, chats: action.chats }; return { ...state, chats: action.chats };
...@@ -44,19 +52,21 @@ function reducer(state, action) { ...@@ -44,19 +52,21 @@ function reducer(state, action) {
return { ...state, chats: updated }; return { ...state, chats: updated };
} }
case "REMOVE_CHAT":
case "DELETE_CHAT": { case "DELETE_CHAT": {
const filtered = state.chats.filter((c) => c.id !== action.chatId); const chatId = action.chatId;
const filtered = state.chats.filter((c) => c.id !== chatId);
const newMessages = { ...state.chatMessages }; const newMessages = { ...state.chatMessages };
delete newMessages[action.chatId]; delete newMessages[chatId];
const newStreams = { ...state.activeStreams }; const newStreams = { ...state.activeStreams };
delete newStreams[action.chatId]; delete newStreams[chatId];
return { return {
...state, ...state,
chats: filtered, chats: filtered,
chatMessages: newMessages, chatMessages: newMessages,
activeStreams: newStreams, activeStreams: newStreams,
activeChatId: activeChatId:
state.activeChatId === action.chatId state.activeChatId === chatId
? filtered[0]?.id || null ? filtered[0]?.id || null
: state.activeChatId, : state.activeChatId,
}; };
...@@ -68,7 +78,6 @@ function reducer(state, action) { ...@@ -68,7 +78,6 @@ function reducer(state, action) {
case "TOGGLE_SIDEBAR": case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen }; return { ...state, sidebarOpen: !state.sidebarOpen };
// ── Per-chat message management ──────────────
case "SET_MESSAGES": case "SET_MESSAGES":
return { return {
...state, ...state,
...@@ -90,8 +99,6 @@ function reducer(state, action) { ...@@ -90,8 +99,6 @@ function reducer(state, action) {
}, },
}; };
// ── Background streaming flags ───────────────
// NOW PER-CHAT — no longer blocks other chats
case "SET_STREAMING": { case "SET_STREAMING": {
if (action.streaming) { if (action.streaming) {
return { return {
...@@ -112,7 +119,6 @@ function reducer(state, action) { ...@@ -112,7 +119,6 @@ function reducer(state, action) {
export function AppProvider({ children }) { export function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
// Give the background stream manager access to dispatch
useEffect(() => { useEffect(() => {
setDispatch(dispatch); setDispatch(dispatch);
}, [dispatch]); }, [dispatch]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment