Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/(app)/admin/admin-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function AdminNav({ horizontal }: { horizontal?: boolean }) {
<Link
key={item.href}
href={item.href}
aria-current={active ? "page" : undefined}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md whitespace-nowrap transition-colors ${
active
? "bg-accent text-foreground"
Expand All @@ -63,7 +64,7 @@ export function AdminNav({ horizontal }: { horizontal?: boolean }) {
<nav className="w-full flex flex-col gap-5">
{sections.map((group) => (
<div key={group.group}>
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-semibold mb-1 px-2">
<p className="text-xs uppercase tracking-wider text-muted-foreground font-semibold mb-1 px-2">
{group.group}
</p>
<ul className="space-y-0.5">
Expand All @@ -77,6 +78,7 @@ export function AdminNav({ horizontal }: { horizontal?: boolean }) {
<li key={item.href}>
<Link
href={item.href}
aria-current={active ? "page" : undefined}
className={`flex items-center gap-2.5 h-9 px-2.5 rounded-md text-sm transition-colors ${
active
? "bg-accent text-foreground font-medium"
Expand Down
1 change: 1 addition & 0 deletions src/app/(app)/admin/cabinet/cabinet-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function CabinetForm({
maxLength={120}
defaultValue={initial?.name ?? "Cabinet"}
placeholder="Votre cabinet"
aria-invalid={state?.ok === false}
/>
<p className="text-xs text-muted-foreground">
Affiché dans l&apos;UI et utilisé par défaut dans le footer des
Expand Down
9 changes: 8 additions & 1 deletion src/app/(app)/admin/users/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,14 @@ export default async function AdminUserDetailPage({
</span>
</p>
</div>
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-2 w-full rounded-full bg-muted overflow-hidden"
role="progressbar"
aria-label="Consommation du quota"
aria-valuenow={monthCostCents}
aria-valuemin={0}
aria-valuemax={quotaCents ?? undefined}
>
<div
className={`h-full transition-all ${
quotaPercent! >= 100
Expand Down
53 changes: 52 additions & 1 deletion src/app/(app)/admin/users/user-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,50 @@ export function UserRow({
{feedback && (
<div className="text-xs text-success mt-1">{feedback}</div>
)}

{/* Résumé compact des stats sur mobile : la rangée détaillée
(Conv./Docs/Projets/Ce mois) est masquée < md, on reflow donc
les chiffres ici pour ne pas les perdre sur petit écran. */}
<div className="md:hidden mt-1.5 flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] tabular-nums text-muted-foreground">
<span>
<span className="opacity-70">Conv. </span>
<span className="font-medium text-foreground">
{entry.stats.convCount}
</span>
</span>
<span>
<span className="opacity-70">Docs </span>
<span className="font-medium text-foreground">
{entry.stats.docCount}
</span>
</span>
<span>
<span className="opacity-70">Projets </span>
<span className="font-medium text-foreground">
{entry.stats.projectCount}
</span>
</span>
<span>
<span className="opacity-70">Ce mois </span>
<span
className={`font-medium ${
quotaCents != null &&
quotaCents > 0 &&
monthSpentCents >= quotaCents
? "text-destructive"
: "text-foreground"
}`}
>
{formatEurFromCents(monthSpentCents)}
{quotaCents != null && (
<span className="font-normal opacity-70">
{" / "}
{formatEurFromCents(quotaCents)}
</span>
)}
</span>
</span>
</div>
</div>

{/* Stats compactes : 4 chiffres en tabular-nums. Cabinet-friendly :
Expand Down Expand Up @@ -250,7 +294,14 @@ export function UserRow({
)}
</div>
{quotaPercent != null && (
<div className="mt-1 h-1 w-20 rounded-full bg-muted overflow-hidden">
<div
className="mt-1 h-1 w-20 rounded-full bg-muted overflow-hidden"
role="progressbar"
aria-label="Consommation du quota"
aria-valuenow={monthSpentCents}
aria-valuemin={0}
aria-valuemax={quotaCents ?? undefined}
>
<div
className={`h-full transition-all ${
quotaPercent >= 100
Expand Down
2 changes: 2 additions & 0 deletions src/app/(app)/admin/users/users-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function UsersTable({ rows, currentUserId, nowMs }: Props) {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Rechercher par email ou nom…"
aria-label="Rechercher un utilisateur"
className="w-full rounded-md border border-input bg-background pl-8 pr-3 py-1.5 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
/>
</div>
Expand All @@ -78,6 +79,7 @@ export function UsersTable({ rows, currentUserId, nowMs }: Props) {
key={f}
type="button"
onClick={() => setFilter(f)}
aria-pressed={filter === f}
className={`px-2.5 py-1.5 rounded-md text-xs transition-colors ${
filter === f
? "bg-foreground text-background"
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/board/[id]/add-agent-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function AddAgentDialog({
<Button
type="button"
variant="default"
className="shadow-lg shadow-foreground/10 rounded-full pl-3 pr-4 h-10"
className="shadow-sm pl-3 pr-4 h-10"
>
<IconPlus className="size-4" />
Ajouter un agent
Expand Down
29 changes: 26 additions & 3 deletions src/app/(app)/board/[id]/agent-flow-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import { memo } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { IconPencil, IconTrash } from "@tabler/icons-react";
import {
IconAlertTriangle,
IconCheck,
IconPencil,
IconTrash,
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import type { PipelineAgent, ProviderKey } from "@/db/schema";
import { roleMeta } from "../agent-role-meta";
Expand Down Expand Up @@ -55,10 +60,28 @@ function AgentFlowNodeBase({ data }: NodeProps) {
className="absolute top-2 right-2 z-10 flex size-2"
aria-label="Agent actif"
>
<span className="absolute inline-flex size-full animate-pulse rounded-full bg-foreground/30 opacity-75" />
<span className="absolute inline-flex size-full motion-safe:animate-pulse rounded-full bg-foreground/30 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-foreground/80" />
</span>
)}
{state === "done" && (
<span
className="absolute top-2 right-2 z-10 inline-flex items-center gap-0.5 text-[10px] uppercase tracking-wider text-success"
aria-label="Agent terminé"
>
<IconCheck className="size-3.5" />
Terminé
</span>
)}
{state === "error" && (
<span
className="absolute top-2 right-2 z-10 inline-flex items-center gap-0.5 text-[10px] uppercase tracking-wider text-destructive"
aria-label="Agent en erreur"
>
<IconAlertTriangle className="size-3.5" />
Erreur
</span>
)}
<Handle
type="target"
position={Position.Left}
Expand Down Expand Up @@ -107,7 +130,7 @@ function AgentFlowNodeBase({ data }: NodeProps) {
e.stopPropagation();
onDelete();
}}
className="size-7 grid place-items-center rounded-md hover:bg-destructive/10 hover:text-destructive transition-colors"
className="size-9 grid place-items-center rounded-md hover:bg-destructive/10 hover:text-destructive transition-colors"
aria-label={`Supprimer ${agent.label}`}
>
<IconTrash className="size-3.5" />
Expand Down
4 changes: 3 additions & 1 deletion src/app/(app)/board/[id]/animated-edge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@xyflow/react";
import { useReducedMotion } from "motion/react";

/**
* Edge custom React Flow avec animation "particule" qui voyage du source
Expand Down Expand Up @@ -35,6 +36,7 @@ export function AnimatedEdge(props: EdgeProps) {

const isActive = Boolean(data?.active);
const isDashed = Boolean(data?.dashed);
const reducedMotion = useReducedMotion();

return (
<>
Expand All @@ -50,7 +52,7 @@ export function AnimatedEdge(props: EdgeProps) {
}}
markerEnd={markerEnd}
/>
{isActive && (
{isActive && !reducedMotion && (
<circle
r="3"
fill="var(--color-foreground)"
Expand Down
5 changes: 1 addition & 4 deletions src/app/(app)/board/[id]/inline-rename.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ export function InlineRename({

if (!editable) {
return (
<h1
className="font-heading text-3xl md:text-4xl tracking-tight"
title={`slug: ${pipelineId}`}
>
<h1 className="font-heading text-3xl md:text-4xl tracking-tight">
{initialName}
</h1>
);
Expand Down
9 changes: 5 additions & 4 deletions src/app/(app)/board/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Link from "next/link";
import { asc, eq } from "drizzle-orm";
import { IconArrowLeft } from "@tabler/icons-react";
import { IconArrowLeft, IconBulb, IconInfoCircle } from "@tabler/icons-react";
import { redirect, notFound } from "next/navigation";
import { auth } from "@/auth";
import { db } from "@/db";
Expand Down Expand Up @@ -87,7 +87,7 @@ export default async function PipelineEditorPage({
{data.pipeline.isPreset && (
<div className="mb-6 rounded-lg border border-dashed border-border/80 bg-muted/20 p-4 flex items-start gap-3">
<div className="size-8 rounded-md grid place-items-center bg-foreground/5 shrink-0">
<span className="text-xs font-mono">i</span>
<IconInfoCircle className="size-4 text-foreground/70" />
</div>
<div className="text-sm">
<p className="font-medium">
Expand Down Expand Up @@ -123,8 +123,9 @@ export default async function PipelineEditorPage({
</div>

<div className="mt-5 text-[11px] text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1">
<span>
💡 Cliquez un agent pour l&apos;éditer
<span className="inline-flex items-center gap-1">
<IconBulb className="size-3.5" />
Cliquez un agent pour l&apos;éditer
</span>
{data.pipeline.mode === "sequential" && !data.pipeline.isPreset && (
<span>· Glissez les cartes pour réordonner</span>
Expand Down
40 changes: 34 additions & 6 deletions src/app/(app)/board/[id]/pipeline-mode-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useTransition } from "react";
import { useRef, useTransition } from "react";
import { useRouter } from "next/navigation";
import { IconAlertTriangle } from "@tabler/icons-react";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -38,6 +39,25 @@ export function PipelineModeBar({ pipeline, agentCount }: PipelineModeBarProps)
}

const modes: PipelineModeKey[] = ["sequential", "council", "parallel"];
const radioRefs = useRef<(HTMLButtonElement | null)[]>([]);

function handleRadioKeyDown(
e: React.KeyboardEvent<HTMLButtonElement>,
index: number
) {
if (isPreset || pending) return;
let nextIndex: number | null = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
nextIndex = (index + 1) % modes.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
nextIndex = (index - 1 + modes.length) % modes.length;
}
if (nextIndex === null) return;
e.preventDefault();
const nextMode = modes[nextIndex];
radioRefs.current[nextIndex]?.focus();
if (nextMode !== mode) update({ mode: nextMode });
}

return (
<div className="py-4 border-y border-border">
Expand Down Expand Up @@ -73,20 +93,25 @@ export function PipelineModeBar({ pipeline, agentCount }: PipelineModeBarProps)
aria-label="Mode d'orchestration"
className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2"
>
{modes.map((m) => {
{modes.map((m, i) => {
const meta = MODE_META[m];
const Icon = meta.icon;
const selected = mode === m;
const disabled = isPreset || pending;
return (
<button
key={m}
ref={(el) => {
radioRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={selected}
aria-label={`${meta.label} — ${meta.pitch}`}
tabIndex={selected ? 0 : -1}
disabled={disabled}
onClick={() => !selected && update({ mode: m })}
onKeyDown={(e) => handleRadioKeyDown(e, i)}
className={cn(
"group relative flex items-start gap-3 rounded-xl border px-4 py-3 text-left transition-all",
selected
Expand Down Expand Up @@ -123,15 +148,18 @@ export function PipelineModeBar({ pipeline, agentCount }: PipelineModeBarProps)
</div>

{mode === "council" && (
<p className="mt-3 text-[11px] text-muted-foreground border-t border-border/40 pt-2">
<p className="mt-3 flex items-start gap-1 text-[11px] text-muted-foreground border-t border-border/40 pt-2">
{(() => {
const debaters = Math.max(0, agentCount - 1);
const calls = rounds * debaters + 1;
return (
<>
⚠️ Coût estimé : {calls} appel{calls > 1 ? "s" : ""} LLM par
question — {debaters} débatteur{debaters > 1 ? "s" : ""} sur{" "}
{rounds} tour{rounds > 1 ? "s" : ""}, plus 1 synthèse finale.
<IconAlertTriangle className="size-3.5 shrink-0 mt-px text-warning" />
<span>
Coût estimé : {calls} appel{calls > 1 ? "s" : ""} LLM par
question — {debaters} débatteur{debaters > 1 ? "s" : ""} sur{" "}
{rounds} tour{rounds > 1 ? "s" : ""}, plus 1 synthèse finale.
</span>
</>
);
})()}
Expand Down
4 changes: 2 additions & 2 deletions src/app/(app)/board/[id]/pipeline-workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function PipelineWorkflowInner({
{/* Vignette radiale subtile pour donner du caractère au canvas */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_30%,transparent,oklch(var(--color-foreground)/0.02)_70%,oklch(var(--color-foreground)/0.04))]"
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_30%,transparent,color-mix(in_oklch,var(--color-foreground)_2%,transparent)_70%,color-mix(in_oklch,var(--color-foreground)_4%,transparent))]"
/>

{/* Bouton reset : visible dès qu'au moins un agent a des
Expand Down Expand Up @@ -409,7 +409,7 @@ function PipelineWorkflowInner({
pannable
zoomable
className="!bg-card !border !border-border"
maskColor="rgb(0 0 0 / 0.05)"
maskColor="color-mix(in oklab, var(--foreground) 6%, transparent)"
nodeColor="var(--color-foreground)"
/>
)}
Expand Down
29 changes: 20 additions & 9 deletions src/app/(app)/board/agent-edit-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,26 @@ export function AgentEditSheet({
maxLength={8000}
aria-describedby={`prompt-help-${agent.id}`}
/>
{systemPrompt.length > 2000 && (
<p
id={`prompt-help-${agent.id}`}
className="text-xs text-muted-foreground"
>
⚠️ Ce prompt sera répété à chaque appel de cet agent — un
prompt long multiplie les coûts en mode council/parallel.
</p>
)}
<p
id={`prompt-help-${agent.id}`}
className={
systemPrompt.length > 2000
? "flex items-start gap-1 text-xs text-warning"
: "text-xs text-muted-foreground"
}
>
{systemPrompt.length > 2000 ? (
<>
<IconAlertTriangle className="size-3.5 shrink-0 mt-px" />
<span>
Ce prompt sera répété à chaque appel de cet agent — un
prompt long multiplie les coûts en mode council/parallel.
</span>
</>
) : (
"Vide = prompt « factory » du rôle. Plus le prompt est long, plus chaque appel coûte cher."
)}
</p>
</div>

<div className="space-y-2">
Expand Down
Loading
Loading