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
@@ -0,0 +1,98 @@
|
||||
import { useFetch } from "../hooks";
|
||||
import { fmtCost, fmtPercent, useT } from "../i18n";
|
||||
|
||||
interface BudgetRow {
|
||||
provider: string;
|
||||
budget: number | null;
|
||||
spent: number;
|
||||
remaining: number | null;
|
||||
share: number | null;
|
||||
warning: boolean;
|
||||
exceeded: boolean;
|
||||
images?: boolean;
|
||||
remote?: { currency?: string; remaining?: number | null; limit?: number | null; used?: number | null; spent_month?: number; scope?: string; error?: string };
|
||||
}
|
||||
|
||||
interface BudgetData {
|
||||
currency: string;
|
||||
total: BudgetRow;
|
||||
providers: BudgetRow[];
|
||||
}
|
||||
|
||||
function Bar({ share, warning, exceeded }: { share: number; warning: boolean; exceeded: boolean }) {
|
||||
const color = exceeded ? "var(--red)" : warning ? "var(--accent2)" : "var(--green)";
|
||||
return (
|
||||
<div className="budget-bar" role="meter" aria-valuenow={Math.round(share * 100)} aria-valuemin={0} aria-valuemax={100}>
|
||||
<div style={{ width: `${Math.min(share, 1) * 100}%`, background: color }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Remote({ r, t }: { r: BudgetRow["remote"]; t: ReturnType<typeof useT> }) {
|
||||
if (!r) return <span className="muted">–</span>;
|
||||
if (r.error) return <span className="muted" title={r.error}>⚠ {t("budget.remoteError")}</span>;
|
||||
const cur = r.currency ?? "USD";
|
||||
if (r.spent_month !== undefined) return <span title={t("budget.orgWide")}>{t("budget.providerSpent", { v: fmtCost(r.spent_month, cur) })}</span>;
|
||||
if (r.remaining !== null && r.remaining !== undefined) return <span>{t("budget.balance", { v: fmtCost(r.remaining, cur) })}</span>;
|
||||
if (r.used !== null && r.used !== undefined) return <span>{t("budget.providerSpent", { v: fmtCost(r.used, cur) })}</span>;
|
||||
return <span className="muted">–</span>;
|
||||
}
|
||||
|
||||
/** Monthly budgets per provider (admins). */
|
||||
export function BudgetPanel() {
|
||||
const t = useT();
|
||||
const { data, error } = useFetch<BudgetData>("/budget");
|
||||
if (error || !data) return null;
|
||||
const rows = [data.total, ...data.providers];
|
||||
return (
|
||||
<div className="panel">
|
||||
<h3>{t("budget.title")}</h3>
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table className="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("stats.provider")}</th>
|
||||
<th className="num">{t("budget.spent")}</th>
|
||||
<th className="num">{t("budget.budget")}</th>
|
||||
<th style={{ minWidth: 140 }} />
|
||||
<th className="num">{t("budget.remaining")}</th>
|
||||
<th>{t("budget.provider")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.provider}>
|
||||
<td>
|
||||
{r.provider === "total" ? <b>{t("stats.total")}</b> : r.provider}
|
||||
{r.images && <span className="chip" style={{ marginLeft: 6 }}>{t("nav.images")}</span>}
|
||||
</td>
|
||||
<td className="num">{fmtCost(r.spent, data.currency)}</td>
|
||||
<td className="num">{r.budget ? fmtCost(r.budget, data.currency) : "–"}</td>
|
||||
<td>{r.share !== null && <Bar share={r.share} warning={r.warning} exceeded={r.exceeded} />}</td>
|
||||
<td className="num" style={{ color: r.exceeded ? "var(--red)" : r.warning ? "var(--accent2)" : undefined }}>
|
||||
{r.remaining !== null ? fmtCost(r.remaining, data.currency) : "–"}
|
||||
{r.share !== null && <span className="muted"> ({fmtPercent(r.share)})</span>}
|
||||
</td>
|
||||
<td>{r.provider === "total" ? null : <Remote r={r.remote} t={t} />}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--muted)", marginTop: 6 }}>{t("budget.hint")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Small header chip when the overall budget is nearly used up. */
|
||||
export function BudgetChip() {
|
||||
const t = useT();
|
||||
const { data } = useFetch<BudgetData>("/budget");
|
||||
const worst = data ? [data.total, ...data.providers].filter((r) => r.warning).sort((a, b) => (b.share ?? 0) - (a.share ?? 0))[0] : undefined;
|
||||
if (!worst) return null;
|
||||
return (
|
||||
<span className="chip warn hide-mobile" title={t("budget.chipHint")}>
|
||||
{t("budget.chip", { name: worst.provider === "total" ? t("stats.total") : worst.provider, p: fmtPercent(worst.share ?? 0) })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user