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

again

parent 25a6e3a4
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -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