@creezio/shell-ui 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +336 -0
  2. package/dist/adapters/nav-shell.d.ts +54 -0
  3. package/dist/adapters/nav-shell.d.ts.map +1 -0
  4. package/dist/adapters/nav-shell.js +112 -0
  5. package/dist/adapters/nav-shell.js.map +1 -0
  6. package/dist/brand.d.ts +64 -0
  7. package/dist/brand.d.ts.map +1 -0
  8. package/dist/brand.js +59 -0
  9. package/dist/brand.js.map +1 -0
  10. package/dist/core-nav.d.ts +17 -0
  11. package/dist/core-nav.d.ts.map +1 -0
  12. package/dist/core-nav.js +40 -0
  13. package/dist/core-nav.js.map +1 -0
  14. package/dist/index.d.ts +29 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +24 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/api-scopes.d.ts +24 -0
  19. package/dist/lib/api-scopes.d.ts.map +1 -0
  20. package/dist/lib/api-scopes.js +78 -0
  21. package/dist/lib/api-scopes.js.map +1 -0
  22. package/dist/lib/catalog-suspense-key.d.ts +3 -0
  23. package/dist/lib/catalog-suspense-key.d.ts.map +1 -0
  24. package/dist/lib/catalog-suspense-key.js +12 -0
  25. package/dist/lib/catalog-suspense-key.js.map +1 -0
  26. package/dist/lib/data-changed.d.ts +51 -0
  27. package/dist/lib/data-changed.d.ts.map +1 -0
  28. package/dist/lib/data-changed.js +227 -0
  29. package/dist/lib/data-changed.js.map +1 -0
  30. package/dist/lib/desktop-home-path.d.ts +14 -0
  31. package/dist/lib/desktop-home-path.d.ts.map +1 -0
  32. package/dist/lib/desktop-home-path.js +29 -0
  33. package/dist/lib/desktop-home-path.js.map +1 -0
  34. package/dist/lib/geo-distance.d.ts +6 -0
  35. package/dist/lib/geo-distance.d.ts.map +1 -0
  36. package/dist/lib/geo-distance.js +27 -0
  37. package/dist/lib/geo-distance.js.map +1 -0
  38. package/dist/lib/img.d.ts +2 -0
  39. package/dist/lib/img.d.ts.map +1 -0
  40. package/dist/lib/img.js +40 -0
  41. package/dist/lib/img.js.map +1 -0
  42. package/dist/lib/keepalive-eviction.d.ts +14 -0
  43. package/dist/lib/keepalive-eviction.d.ts.map +1 -0
  44. package/dist/lib/keepalive-eviction.js +44 -0
  45. package/dist/lib/keepalive-eviction.js.map +1 -0
  46. package/dist/lib/ops-track.d.ts +29 -0
  47. package/dist/lib/ops-track.d.ts.map +1 -0
  48. package/dist/lib/ops-track.js +37 -0
  49. package/dist/lib/ops-track.js.map +1 -0
  50. package/dist/lib/optimize-cover-url.d.ts +10 -0
  51. package/dist/lib/optimize-cover-url.d.ts.map +1 -0
  52. package/dist/lib/optimize-cover-url.js +80 -0
  53. package/dist/lib/optimize-cover-url.js.map +1 -0
  54. package/dist/lib/page-trails.d.ts +9 -0
  55. package/dist/lib/page-trails.d.ts.map +1 -0
  56. package/dist/lib/page-trails.js +20 -0
  57. package/dist/lib/page-trails.js.map +1 -0
  58. package/dist/lib/public-origin.d.ts +32 -0
  59. package/dist/lib/public-origin.d.ts.map +1 -0
  60. package/dist/lib/public-origin.js +125 -0
  61. package/dist/lib/public-origin.js.map +1 -0
  62. package/dist/lib/server-incident.d.ts +12 -0
  63. package/dist/lib/server-incident.d.ts.map +1 -0
  64. package/dist/lib/server-incident.js +72 -0
  65. package/dist/lib/server-incident.js.map +1 -0
  66. package/dist/lib/tab-document-url.d.ts +13 -0
  67. package/dist/lib/tab-document-url.d.ts.map +1 -0
  68. package/dist/lib/tab-document-url.js +60 -0
  69. package/dist/lib/tab-document-url.js.map +1 -0
  70. package/dist/lib/utils.d.ts +20 -0
  71. package/dist/lib/utils.d.ts.map +1 -0
  72. package/dist/lib/utils.js +87 -0
  73. package/dist/lib/utils.js.map +1 -0
  74. package/dist/nav-catalog.d.ts +97 -0
  75. package/dist/nav-catalog.d.ts.map +1 -0
  76. package/dist/nav-catalog.js +282 -0
  77. package/dist/nav-catalog.js.map +1 -0
  78. package/dist/registry.d.ts +11 -0
  79. package/dist/registry.d.ts.map +1 -0
  80. package/dist/registry.js +60 -0
  81. package/dist/registry.js.map +1 -0
  82. package/dist/types.d.ts +22 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +2 -0
  85. package/dist/types.js.map +1 -0
  86. package/dist-cjs/adapters/nav-shell.js +115 -0
  87. package/dist-cjs/adapters/nav-shell.js.map +1 -0
  88. package/dist-cjs/brand.js +66 -0
  89. package/dist-cjs/brand.js.map +1 -0
  90. package/dist-cjs/core-nav.js +43 -0
  91. package/dist-cjs/core-nav.js.map +1 -0
  92. package/dist-cjs/index.js +95 -0
  93. package/dist-cjs/index.js.map +1 -0
  94. package/dist-cjs/lib/api-scopes.js +86 -0
  95. package/dist-cjs/lib/api-scopes.js.map +1 -0
  96. package/dist-cjs/lib/catalog-suspense-key.js +15 -0
  97. package/dist-cjs/lib/catalog-suspense-key.js.map +1 -0
  98. package/dist-cjs/lib/data-changed.js +236 -0
  99. package/dist-cjs/lib/data-changed.js.map +1 -0
  100. package/dist-cjs/lib/desktop-home-path.js +34 -0
  101. package/dist-cjs/lib/desktop-home-path.js.map +1 -0
  102. package/dist-cjs/lib/geo-distance.js +30 -0
  103. package/dist-cjs/lib/geo-distance.js.map +1 -0
  104. package/dist-cjs/lib/img.js +43 -0
  105. package/dist-cjs/lib/img.js.map +1 -0
  106. package/dist-cjs/lib/keepalive-eviction.js +49 -0
  107. package/dist-cjs/lib/keepalive-eviction.js.map +1 -0
  108. package/dist-cjs/lib/ops-track.js +41 -0
  109. package/dist-cjs/lib/ops-track.js.map +1 -0
  110. package/dist-cjs/lib/optimize-cover-url.js +83 -0
  111. package/dist-cjs/lib/optimize-cover-url.js.map +1 -0
  112. package/dist-cjs/lib/page-trails.js +25 -0
  113. package/dist-cjs/lib/page-trails.js.map +1 -0
  114. package/dist-cjs/lib/public-origin.js +130 -0
  115. package/dist-cjs/lib/public-origin.js.map +1 -0
  116. package/dist-cjs/lib/server-incident.js +75 -0
  117. package/dist-cjs/lib/server-incident.js.map +1 -0
  118. package/dist-cjs/lib/tab-document-url.js +65 -0
  119. package/dist-cjs/lib/tab-document-url.js.map +1 -0
  120. package/dist-cjs/lib/utils.js +97 -0
  121. package/dist-cjs/lib/utils.js.map +1 -0
  122. package/dist-cjs/nav-catalog.js +293 -0
  123. package/dist-cjs/nav-catalog.js.map +1 -0
  124. package/dist-cjs/package.json +3 -0
  125. package/dist-cjs/registry.js +64 -0
  126. package/dist-cjs/registry.js.map +1 -0
  127. package/dist-cjs/types.js +3 -0
  128. package/dist-cjs/types.js.map +1 -0
  129. package/package.json +163 -0
  130. package/ui/app-error-boundary.tsx +59 -0
  131. package/ui/data-table.tsx +121 -0
  132. package/ui/desktop/auth-window-chrome.tsx +51 -0
  133. package/ui/desktop/desktop-bridge.tsx +87 -0
  134. package/ui/desktop/external-site-slot.tsx +313 -0
  135. package/ui/desktop/external-site-surface.ts +58 -0
  136. package/ui/desktop/site-link.tsx +105 -0
  137. package/ui/desktop/window-chrome-controls.tsx +5 -0
  138. package/ui/desktop-types.ts +30 -0
  139. package/ui/faceted-filters.tsx +119 -0
  140. package/ui/global-search-host.ts +20 -0
  141. package/ui/global-search.tsx +66 -0
  142. package/ui/index.ts +178 -0
  143. package/ui/kit.ts +30 -0
  144. package/ui/layout/app-shell.tsx +58 -0
  145. package/ui/layout/desktop-update-banner.tsx +100 -0
  146. package/ui/layout/entity-header.tsx +44 -0
  147. package/ui/layout/native-os-nav.ts +104 -0
  148. package/ui/layout/nav-catalog-loader.tsx +98 -0
  149. package/ui/layout/nav-icons.ts +67 -0
  150. package/ui/layout/page-chrome.tsx +90 -0
  151. package/ui/layout/page-toolbar-context.tsx +82 -0
  152. package/ui/layout/sandbox-banner.tsx +38 -0
  153. package/ui/layout/section-view-shell.tsx +73 -0
  154. package/ui/layout/sidebar-host.ts +144 -0
  155. package/ui/layout/sidebar.tsx +1150 -0
  156. package/ui/lib/ai-screencast-hub.ts +127 -0
  157. package/ui/lib/ai-workspace-client.ts +46 -0
  158. package/ui/lib/aid.ts +22 -0
  159. package/ui/lib/desktop-host.ts +114 -0
  160. package/ui/lib/fleet-tracker-client.ts +237 -0
  161. package/ui/lib/hermes-ui.ts +114 -0
  162. package/ui/lib/n8n-ui.ts +115 -0
  163. package/ui/lib/use-creezio-resource.tsx +121 -0
  164. package/ui/lib/use-shell-ui-brand.ts +22 -0
  165. package/ui/list-toolbar.tsx +309 -0
  166. package/ui/os-pages/desktop-settings-page.tsx +158 -0
  167. package/ui/os-pages/index.ts +8 -0
  168. package/ui/page-loading/entity-page-loading.tsx +59 -0
  169. package/ui/page-loading/list-page-loading.tsx +61 -0
  170. package/ui/pagination.tsx +56 -0
  171. package/ui/primitives/avatar.tsx +33 -0
  172. package/ui/primitives/badge.tsx +36 -0
  173. package/ui/primitives/breadcrumb.tsx +93 -0
  174. package/ui/primitives/button.tsx +44 -0
  175. package/ui/primitives/card.tsx +44 -0
  176. package/ui/primitives/chart.tsx +317 -0
  177. package/ui/primitives/command.tsx +111 -0
  178. package/ui/primitives/dialog.tsx +100 -0
  179. package/ui/primitives/dropdown-menu.tsx +187 -0
  180. package/ui/primitives/input.tsx +19 -0
  181. package/ui/primitives/label.tsx +17 -0
  182. package/ui/primitives/resizable.tsx +45 -0
  183. package/ui/primitives/scroll-area.tsx +41 -0
  184. package/ui/primitives/select.tsx +73 -0
  185. package/ui/primitives/separator.tsx +26 -0
  186. package/ui/primitives/sheet.tsx +90 -0
  187. package/ui/primitives/skeleton.tsx +7 -0
  188. package/ui/primitives/sonner.tsx +22 -0
  189. package/ui/primitives/tabs.tsx +51 -0
  190. package/ui/primitives/textarea.tsx +22 -0
  191. package/ui/primitives/tooltip.tsx +31 -0
  192. package/ui/pwa/client-error-reporter.tsx +62 -0
  193. package/ui/pwa/register-sw.tsx +50 -0
  194. package/ui/range-filters.tsx +174 -0
  195. package/ui/search/global-search-config.ts +32 -0
  196. package/ui/search/global-search-provider.tsx +528 -0
  197. package/ui/search/search-history.ts +74 -0
  198. package/ui/search-input.tsx +146 -0
  199. package/ui/settings/account-settings.tsx +140 -0
  200. package/ui/settings/agent-profile-settings.tsx +184 -0
  201. package/ui/settings/api-keys-settings.tsx +349 -0
  202. package/ui/settings/desktop-background-settings.tsx +128 -0
  203. package/ui/settings/desktop-connection-settings.tsx +251 -0
  204. package/ui/settings/desktop-embed-env-panel.tsx +257 -0
  205. package/ui/settings/desktop-fleet-telemetry-settings.tsx +269 -0
  206. package/ui/settings/desktop-hermes-settings.tsx +341 -0
  207. package/ui/settings/desktop-llm-keys.tsx +192 -0
  208. package/ui/settings/desktop-n8n-settings.tsx +306 -0
  209. package/ui/settings/desktop-tunnel.tsx +264 -0
  210. package/ui/settings/desktop-update-settings.tsx +175 -0
  211. package/ui/settings/factory-reset-settings.tsx +89 -0
  212. package/ui/settings/host-only-settings.tsx +62 -0
  213. package/ui/settings/locked-config-field.tsx +30 -0
  214. package/ui/settings/ops-diagnostic-settings.tsx +242 -0
  215. package/ui/settings/search-reindex-settings.tsx +158 -0
  216. package/ui/settings/server-mode-cards.tsx +801 -0
  217. package/ui/theme/tailwind-preset.cjs +82 -0
  218. package/ui/theme/theme.css +328 -0
  219. package/ui/workspace/ai-activity-panel-host.tsx +19 -0
  220. package/ui/workspace/ai-workspace-agent-host.tsx +54 -0
  221. package/ui/workspace/ai-workspace-banner.tsx +168 -0
  222. package/ui/workspace/impersonation-banner.tsx +39 -0
  223. package/ui/workspace/keep-alive.tsx +220 -0
  224. package/ui/workspace/tab-workspace-context.tsx +1172 -0
  225. package/ui/workspace/tab-workspace-host.ts +89 -0
  226. package/ui/workspace/types.ts +500 -0
  227. package/ui/workspace/use-location-search.tsx +50 -0
  228. package/ui/workspace/window-chrome-controls.tsx +116 -0
  229. package/ui/workspace/workspace-config.ts +47 -0
  230. package/ui/workspace/workspace-root.tsx +147 -0
  231. package/ui/workspace/workspace-shell.tsx +292 -0
  232. package/ui/workspace/workspace-tab-bar.tsx +572 -0
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { FormEvent, useEffect, useRef, useState, useTransition, type ChangeEvent } from "react";
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
+ import { Input } from "./primitives/input";
6
+ import { Button } from "./primitives/button";
7
+ import { cn } from "@creezio/shell-ui";
8
+ import { aidProps } from "./lib/aid";
9
+
10
+ export const SEARCH_DEBOUNCE_MS = 300;
11
+
12
+ type SearchInputProps = {
13
+ searchKey?: string;
14
+ placeholder?: string;
15
+ className?: string;
16
+ inputClassName?: string;
17
+ /** Affiche un bouton submit optionnel (défaut : masqué, debounce suffit). */
18
+ showSubmit?: boolean;
19
+ submitLabel?: string;
20
+ debounceMs?: number;
21
+ /**
22
+ * Mode filtre client (données déjà en props) : met à jour l’URL sans
23
+ * `router.refresh()` — évite un re-fetch RSC/SQLite à chaque frappe.
24
+ */
25
+ skipRefresh?: boolean;
26
+ };
27
+
28
+ /**
29
+ * Champ de recherche URL-driven avec debounce (replace, sans empiler l’historique).
30
+ * Centralisé pour les listes ops (clients, véhicules, assurances, etc.).
31
+ *
32
+ * Next.js 14 : `router.replace` met à jour l’URL ; `router.refresh()` force le
33
+ * re-fetch RSC (sinon soft-nav peut laisser le tableau stale). Un seul refresh
34
+ * après replace — pas de history.replaceState seul (refresh ignore alors le `q`).
35
+ */
36
+ export function SearchInput({
37
+ searchKey = "q",
38
+ placeholder = "Rechercher…",
39
+ className,
40
+ inputClassName,
41
+ showSubmit = false,
42
+ submitLabel = "Filtrer",
43
+ debounceMs = SEARCH_DEBOUNCE_MS,
44
+ skipRefresh = false,
45
+ }: SearchInputProps) {
46
+ const router = useRouter();
47
+ const pathname = usePathname();
48
+ const params = useSearchParams();
49
+ const urlSearch = params.get(searchKey) || "";
50
+ const [searchValue, setSearchValue] = useState(urlSearch);
51
+ const [, startTransition] = useTransition();
52
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
53
+ const paramsRef = useRef(params);
54
+ const pathnameRef = useRef(pathname);
55
+ const navSeqRef = useRef(0);
56
+ /** Dernière valeur poussée vers l’URL (ou reçue de l’extérieur). */
57
+ const appliedRef = useRef(urlSearch);
58
+ /** true tant que l’utilisateur tape et que le debounce n’a pas appliqué. */
59
+ const dirtyRef = useRef(false);
60
+ paramsRef.current = params;
61
+ pathnameRef.current = pathname;
62
+
63
+ // Sync input ← URL (back/forward, lien, clear externe) sans écraser une frappe en cours.
64
+ useEffect(() => {
65
+ if (dirtyRef.current) {
66
+ if (urlSearch === searchValue.trim()) {
67
+ dirtyRef.current = false;
68
+ appliedRef.current = urlSearch;
69
+ }
70
+ return;
71
+ }
72
+ if (urlSearch !== appliedRef.current) {
73
+ appliedRef.current = urlSearch;
74
+ setSearchValue(urlSearch);
75
+ }
76
+ }, [urlSearch, searchValue]);
77
+
78
+ function applySearch(raw: string) {
79
+ const q = raw.trim();
80
+ const next = new URLSearchParams(paramsRef.current.toString());
81
+ const current = next.get(searchKey) || "";
82
+ if (q === current) {
83
+ dirtyRef.current = false;
84
+ appliedRef.current = q;
85
+ return;
86
+ }
87
+ if (q) next.set(searchKey, q);
88
+ else next.delete(searchKey);
89
+ next.delete("page");
90
+ const qs = next.toString();
91
+ const href = qs ? `${pathnameRef.current}?${qs}` : pathnameRef.current;
92
+ const seq = ++navSeqRef.current;
93
+ dirtyRef.current = false;
94
+ appliedRef.current = q;
95
+ startTransition(() => {
96
+ if (seq !== navSeqRef.current) return;
97
+ router.replace(href, { scroll: false });
98
+ if (!skipRefresh) router.refresh();
99
+ });
100
+ }
101
+
102
+ useEffect(() => {
103
+ if (searchValue.trim() === urlSearch && !dirtyRef.current) return;
104
+ if (searchValue.trim() === appliedRef.current && !dirtyRef.current) return;
105
+ if (debounceRef.current) clearTimeout(debounceRef.current);
106
+ debounceRef.current = setTimeout(() => {
107
+ applySearch(searchValue);
108
+ }, debounceMs);
109
+ return () => {
110
+ if (debounceRef.current) clearTimeout(debounceRef.current);
111
+ };
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- debounce sur la frappe uniquement
113
+ }, [searchValue, urlSearch, searchKey, debounceMs]);
114
+
115
+ function onSubmit(e: FormEvent) {
116
+ e.preventDefault();
117
+ if (debounceRef.current) clearTimeout(debounceRef.current);
118
+ applySearch(searchValue);
119
+ }
120
+
121
+ return (
122
+ <form onSubmit={onSubmit} className={cn("flex min-w-[200px] flex-1 gap-2", className)}>
123
+ <Input
124
+ name={searchKey}
125
+ value={searchValue}
126
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
127
+ dirtyRef.current = true;
128
+ setSearchValue(e.target.value);
129
+ }}
130
+ placeholder={placeholder}
131
+ {...aidProps(`search.${searchKey}`)}
132
+ className={cn("max-w-sm", inputClassName)}
133
+ autoComplete="off"
134
+ />
135
+ {showSubmit ? (
136
+ <Button type="submit" variant="secondary">
137
+ {submitLabel}
138
+ </Button>
139
+ ) : (
140
+ <button type="submit" className="sr-only">
141
+ Rechercher
142
+ </button>
143
+ )}
144
+ </form>
145
+ );
146
+ }
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Compte local desktop : identifiant + changement de mot de passe
7
+ * (écrit local-config + redémarre Next, comme BYOK).
8
+ */
9
+
10
+ import { FormEvent, useEffect, useState } from "react";
11
+ import { toast } from "sonner";
12
+ import { Loader2, UserRound } from "lucide-react";
13
+ import { Button } from "../primitives/button";
14
+ import { Input } from "../primitives/input";
15
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
16
+
17
+ export function AccountSettings() {
18
+ const [desktop, setDesktop] = useState(false);
19
+ const [username, setUsername] = useState<string | null>(null);
20
+ const [currentPassword, setCurrentPassword] = useState("");
21
+ const [newPassword, setNewPassword] = useState("");
22
+ const [newPassword2, setNewPassword2] = useState("");
23
+ const [busy, setBusy] = useState(false);
24
+
25
+ useEffect(() => {
26
+ const api = getShellDesktopApi();
27
+ if (!api?.getAccount) {
28
+ // Hors desktop : afficher le user session si possible
29
+ void fetch("/api/v1/auth/me")
30
+ .then((r) => (r.ok ? r.json() : null))
31
+ .then((d: { user?: string } | null) => {
32
+ if (d?.user) setUsername(d.user);
33
+ })
34
+ .catch(() => {});
35
+ return;
36
+ }
37
+ setDesktop(true);
38
+ void api.getAccount().then((a: { username: string }) => setUsername(a.username));
39
+ }, []);
40
+
41
+ async function onChangePassword(e: FormEvent) {
42
+ e.preventDefault();
43
+ const api = getShellDesktopApi();
44
+ if (!api?.changePassword) {
45
+ toast.error("Changement de mot de passe disponible dans l'app desktop");
46
+ return;
47
+ }
48
+ if (newPassword.length < 6) {
49
+ toast.error("Nouveau mot de passe trop court (min. 6 caractères)");
50
+ return;
51
+ }
52
+ if (newPassword !== newPassword2) {
53
+ toast.error("Les mots de passe ne correspondent pas");
54
+ return;
55
+ }
56
+ setBusy(true);
57
+ try {
58
+ const r = await api.changePassword({ currentPassword, newPassword });
59
+ if (!r.ok) {
60
+ toast.error(r.error || "Échec du changement de mot de passe");
61
+ return;
62
+ }
63
+ setCurrentPassword("");
64
+ setNewPassword("");
65
+ setNewPassword2("");
66
+ toast.success("Mot de passe mis à jour — serveur local redémarré");
67
+ } catch (err) {
68
+ toast.error(err instanceof Error ? err.message : "Erreur");
69
+ } finally {
70
+ setBusy(false);
71
+ }
72
+ }
73
+
74
+ return (
75
+ <Card>
76
+ <CardHeader>
77
+ <CardTitle className="flex items-center gap-2 text-base">
78
+ <UserRound className="h-4 w-4" /> Compte
79
+ </CardTitle>
80
+ <CardDescription>
81
+ Identifiant local de cette installation. Le mot de passe est stocké de façon sécurisée
82
+ sur cet ordinateur.
83
+ </CardDescription>
84
+ </CardHeader>
85
+ <CardContent className="space-y-4">
86
+ <div>
87
+ <p className="mb-1 text-[13px] font-medium text-slate-700">Identifiant</p>
88
+ <p className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-900">
89
+ {username || "—"}
90
+ </p>
91
+ </div>
92
+ {desktop ? (
93
+ <form onSubmit={(e) => void onChangePassword(e)} className="space-y-3">
94
+ <p className="text-[13px] font-medium text-slate-700">Changer le mot de passe</p>
95
+ <Input
96
+ type="password"
97
+ autoComplete="current-password"
98
+ placeholder="Mot de passe actuel"
99
+ value={currentPassword}
100
+ onChange={(e) => setCurrentPassword(e.target.value)}
101
+ required
102
+ disabled={busy}
103
+ />
104
+ <Input
105
+ type="password"
106
+ autoComplete="new-password"
107
+ placeholder="Nouveau mot de passe"
108
+ value={newPassword}
109
+ onChange={(e) => setNewPassword(e.target.value)}
110
+ required
111
+ disabled={busy}
112
+ />
113
+ <Input
114
+ type="password"
115
+ autoComplete="new-password"
116
+ placeholder="Confirmer le nouveau mot de passe"
117
+ value={newPassword2}
118
+ onChange={(e) => setNewPassword2(e.target.value)}
119
+ required
120
+ disabled={busy}
121
+ />
122
+ <Button type="submit" disabled={busy} variant="outline">
123
+ {busy ? (
124
+ <>
125
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Mise à jour…
126
+ </>
127
+ ) : (
128
+ "Enregistrer le mot de passe"
129
+ )}
130
+ </Button>
131
+ </form>
132
+ ) : (
133
+ <p className="text-sm text-slate-500">
134
+ Le changement de mot de passe se fait dans l&apos;application desktop.
135
+ </p>
136
+ )}
137
+ </CardContent>
138
+ </Card>
139
+ );
140
+ }
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Profil d'agent Work (D3) — par utilisateur.
5
+ * `company` : Hermes embarqué de l'app Serveur (skills creezio-*).
6
+ * `personal` : le Hermes personnel de l'utilisateur (URL + clé API),
7
+ * utilisé à la place du moteur entreprise pour SES sessions Work.
8
+ */
9
+
10
+ import { useEffect, useState } from "react";
11
+ import { toast } from "sonner";
12
+ import { AlertTriangle, Bot, KeyRound } from "lucide-react";
13
+ import { Button } from "../primitives/button";
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ } from "../primitives/card";
21
+ import { Input } from "../primitives/input";
22
+ import { Label } from "../primitives/label";
23
+ import { aidProps } from "../lib/aid";
24
+ import {
25
+ Select,
26
+ SelectContent,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from "../primitives/select";
31
+
32
+ type AgentProfile = {
33
+ kind: "company" | "personal";
34
+ apiUrl: string | null;
35
+ hasKey: boolean;
36
+ incomplete?: boolean;
37
+ };
38
+
39
+ export function AgentProfileSettings() {
40
+ const [loaded, setLoaded] = useState(false);
41
+ const [kind, setKind] = useState<"company" | "personal">("company");
42
+ const [apiUrl, setApiUrl] = useState("");
43
+ const [apiKey, setApiKey] = useState("");
44
+ const [hasKey, setHasKey] = useState(false);
45
+ const [saving, setSaving] = useState(false);
46
+
47
+ useEffect(() => {
48
+ let alive = true;
49
+ void fetch("/api/v1/assistant/agent-profile")
50
+ .then((r) => (r.ok ? (r.json() as Promise<AgentProfile>) : null))
51
+ .then((p) => {
52
+ if (!alive || !p) return;
53
+ setKind(p.kind);
54
+ setApiUrl(p.apiUrl || "");
55
+ setHasKey(p.hasKey);
56
+ })
57
+ .finally(() => alive && setLoaded(true));
58
+ return () => {
59
+ alive = false;
60
+ };
61
+ }, []);
62
+
63
+ const save = async () => {
64
+ setSaving(true);
65
+ try {
66
+ const res = await fetch("/api/v1/assistant/agent-profile", {
67
+ method: "PUT",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({
70
+ kind,
71
+ apiUrl,
72
+ // Champ vide = conserver la clé déjà enregistrée.
73
+ ...(apiKey ? { apiKey } : {}),
74
+ }),
75
+ });
76
+ const data = (await res.json()) as AgentProfile & { error?: string };
77
+ if (!res.ok) {
78
+ toast.error(data.error || "Enregistrement impossible");
79
+ return;
80
+ }
81
+ setHasKey(data.hasKey);
82
+ setApiKey("");
83
+ if (data.incomplete) {
84
+ toast.warning(
85
+ "Profil enregistré mais incomplet : sans URL et clé, l'agent de l'entreprise reste utilisé.",
86
+ );
87
+ } else {
88
+ toast.success(
89
+ data.kind === "personal"
90
+ ? "Mode Work → votre agent personnel"
91
+ : "Mode Work → agent de l'entreprise",
92
+ );
93
+ }
94
+ } catch {
95
+ toast.error("Enregistrement impossible");
96
+ } finally {
97
+ setSaving(false);
98
+ }
99
+ };
100
+
101
+ if (!loaded) return null;
102
+
103
+ return (
104
+ <Card {...aidProps("agent-profile-settings")}>
105
+ <CardHeader>
106
+ <CardTitle className="flex items-center gap-2 text-base">
107
+ <Bot className="h-4 w-4" />
108
+ Agent (mode Work)
109
+ </CardTitle>
110
+ <CardDescription>
111
+ Choisissez quel agent exécute vos missions Work : celui de
112
+ l&apos;entreprise (accès CRM, n8n, modules) ou votre agent Hermes
113
+ personnel.
114
+ </CardDescription>
115
+ </CardHeader>
116
+ <CardContent className="space-y-4">
117
+ <div className="space-y-2">
118
+ <Label>Agent utilisé</Label>
119
+ <Select
120
+ value={kind}
121
+ onValueChange={(v) => setKind(v === "personal" ? "personal" : "company")}
122
+ >
123
+ <SelectTrigger className="w-full sm:w-80">
124
+ <SelectValue />
125
+ </SelectTrigger>
126
+ <SelectContent>
127
+ <SelectItem value="company">
128
+ Agent de l&apos;entreprise (recommandé)
129
+ </SelectItem>
130
+ <SelectItem value="personal">Mon agent personnel</SelectItem>
131
+ </SelectContent>
132
+ </Select>
133
+ </div>
134
+
135
+ {kind === "personal" && (!apiUrl.trim() || (!hasKey && !apiKey)) ? (
136
+ <div className="flex items-start gap-2 rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
137
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
138
+ <span>
139
+ Profil incomplet : renseignez l&apos;URL et la clé API, sinon vos
140
+ missions Work continueront d&apos;utiliser l&apos;agent de
141
+ l&apos;entreprise.
142
+ </span>
143
+ </div>
144
+ ) : null}
145
+
146
+ {kind === "personal" ? (
147
+ <div className="space-y-3 rounded-lg border p-3">
148
+ <div className="space-y-2">
149
+ <Label htmlFor="agent-personal-url">URL de l&apos;API Hermes</Label>
150
+ <Input
151
+ id="agent-personal-url"
152
+ placeholder="https://mon-hermes.exemple.com"
153
+ value={apiUrl}
154
+ onChange={(e) => setApiUrl(e.target.value)}
155
+ />
156
+ </div>
157
+ <div className="space-y-2">
158
+ <Label htmlFor="agent-personal-key" className="flex items-center gap-1.5">
159
+ <KeyRound className="h-3.5 w-3.5" />
160
+ Clé API
161
+ </Label>
162
+ <Input
163
+ id="agent-personal-key"
164
+ type="password"
165
+ placeholder={hasKey ? "•••••••• (enregistrée — laisser vide pour conserver)" : "hermes-…"}
166
+ value={apiKey}
167
+ onChange={(e) => setApiKey(e.target.value)}
168
+ />
169
+ </div>
170
+ <p className="text-xs text-muted-foreground">
171
+ Votre agent personnel n&apos;a pas accès aux données Creezio
172
+ (CRM, n8n, modules) : pour ces missions, repassez sur l&apos;agent
173
+ de l&apos;entreprise.
174
+ </p>
175
+ </div>
176
+ ) : null}
177
+
178
+ <Button onClick={() => void save()} disabled={saving}>
179
+ {saving ? "Enregistrement…" : "Enregistrer"}
180
+ </Button>
181
+ </CardContent>
182
+ </Card>
183
+ );
184
+ }