Users, file manager, image generation, budgets and agent workflow
- User management: local accounts (Argon2) and OpenID Connect, admin/user roles, per-user folders plus admin-shared folders, read-only session sharing, event filtering per user - File manager with CodeMirror editor and git client (status, diff, stage, commit, branches, fetch/pull/push, log, clone/init) - Image generation via fal.ai: generate_image tool and image page - Budgets: monthly limits per provider, provider balances/costs (OpenRouter, Anthropic admin cost report, fal.ai), price discovery - Subagents on other models, background tasks, concurrency limits per host/model/subagent - MCP tools loaded on demand (mcp_search/mcp_call), read-only hints - Permission mode "auto", safe read-only commands always allowed - Persistent message queue with attachments, deletable - Live reasoning incl. <think> tags, activity animations, CSS themes, mobile layout, statistics per user, upload size handling - Docker: named volume for state, localhost-bound port Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3a2fe3816
commit
49262c612c
70 files changed
+8495
-714
No files matched your search
+36
-19
@@ -1,39 +1,48 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ApiError, connectEvents, get, getToken } from "./api";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { connectEvents, get, setUnauthorizedHandler } from "./api";
|
||||
import { useT } from "./i18n";
|
||||
import { navigate, useRoute } from "./router";
|
||||
import { applyEvent, loadSessions, resync, setState } from "./store";
|
||||
import type { Info } from "./types";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { Login } from "./components/Login";
|
||||
import { ChatView } from "./components/ChatView";
|
||||
import { NewSession } from "./components/NewSession";
|
||||
import { Settings } from "./components/Settings";
|
||||
import { Stats } from "./components/Stats";
|
||||
import { Images } from "./components/Images";
|
||||
import { Spinner, useTitleActivity } from "./components/Activity";
|
||||
|
||||
type AuthState = "checking" | "login" | "ok";
|
||||
// The file manager (editor, git) is loaded on demand to keep the chat small.
|
||||
const Files = lazy(() => import("./components/Files").then((m) => ({ default: m.Files })));
|
||||
|
||||
export function App() {
|
||||
const t = useT();
|
||||
const route = useRoute();
|
||||
const [auth, setAuth] = useState<AuthState>("checking");
|
||||
const [info, setInfo] = useState<Info>();
|
||||
const [error, setError] = useState<string>();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
useTitleActivity();
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const i = await get<Info>("/info");
|
||||
setState(() => ({ me: i.user }));
|
||||
if (i.user) await loadSessions();
|
||||
setInfo(i);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Check whether a token is needed and whether ours works.
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const info = await get<{ auth_required: boolean }>("/info");
|
||||
if (info.auth_required && !getToken()) return setAuth("login");
|
||||
await loadSessions();
|
||||
setAuth("ok");
|
||||
} catch (e) {
|
||||
setAuth(e instanceof ApiError && e.status === 401 ? "login" : "ok");
|
||||
}
|
||||
})();
|
||||
setUnauthorizedHandler(() => setInfo((i) => (i ? { ...i, user: null } : i)));
|
||||
void refresh();
|
||||
}, []);
|
||||
|
||||
const signedIn = !!info?.user;
|
||||
useEffect(() => {
|
||||
if (auth !== "ok") return;
|
||||
if (!signedIn) return;
|
||||
return connectEvents({
|
||||
onEvent: applyEvent,
|
||||
onOpen: () => {
|
||||
@@ -42,19 +51,27 @@ export function App() {
|
||||
},
|
||||
onClose: () => setState(() => ({ connected: false })),
|
||||
});
|
||||
}, [auth]);
|
||||
}, [signedIn]);
|
||||
|
||||
// Close the mobile menu on navigation.
|
||||
useEffect(() => setMenuOpen(false), [route.join("/")]);
|
||||
|
||||
if (auth === "checking") return <div className="empty">{t("common.loading")}</div>;
|
||||
if (auth === "login") return <Login onSuccess={() => setAuth("ok")} />;
|
||||
if (error) return <div className="empty">{error}</div>;
|
||||
if (!info) return <div className="empty">{t("common.loading")}</div>;
|
||||
if (!info.user) return <Login info={info} onSuccess={refresh} />;
|
||||
|
||||
const [section, arg] = route;
|
||||
let content;
|
||||
if (section === "s" && arg) content = <ChatView key={arg} sessionId={arg} onMenu={() => setMenuOpen(true)} />;
|
||||
else if (section === "settings") content = <Settings tab={arg} onMenu={() => setMenuOpen(true)} />;
|
||||
else if (section === "stats") content = <Stats onMenu={() => setMenuOpen(true)} />;
|
||||
else if (section === "images") content = <Images onMenu={() => setMenuOpen(true)} />;
|
||||
else if (section === "files")
|
||||
content = (
|
||||
<Suspense fallback={<div className="empty"><Spinner /></div>}>
|
||||
<Files onMenu={() => setMenuOpen(true)} />
|
||||
</Suspense>
|
||||
);
|
||||
else content = <NewSession onMenu={() => setMenuOpen(true)} onCreated={(id) => navigate(`/s/${id}`)} />;
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user