@promptbook/cli 0.103.0-51 → 0.103.0-53

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 (114) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/config.ts +3 -3
  3. package/apps/agents-server/next.config.ts +1 -1
  4. package/apps/agents-server/public/sw.js +16 -0
  5. package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
  6. package/apps/agents-server/src/app/actions.ts +15 -13
  7. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  8. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  9. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  10. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  11. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  12. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  13. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  14. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  15. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +3 -3
  17. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  19. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +12 -12
  29. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  32. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +46 -24
  38. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  39. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  40. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  41. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  42. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  43. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  44. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  45. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  46. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  47. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  48. package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
  49. package/apps/agents-server/src/app/docs/page.tsx +33 -0
  50. package/apps/agents-server/src/app/layout.tsx +29 -3
  51. package/apps/agents-server/src/app/manifest.ts +109 -0
  52. package/apps/agents-server/src/app/page.tsx +8 -45
  53. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  54. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  55. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  56. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  57. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  58. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  59. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  60. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  61. package/apps/agents-server/src/components/Header/Header.tsx +445 -79
  62. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  63. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  64. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  65. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  66. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  67. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  68. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
  69. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  70. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  71. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  72. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  73. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  74. package/apps/agents-server/src/middleware.ts +146 -93
  75. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  76. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  77. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  78. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  79. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  80. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  81. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  82. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  83. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  84. package/apps/agents-server/vercel.json +7 -0
  85. package/esm/index.es.js +344 -11
  86. package/esm/index.es.js.map +1 -1
  87. package/esm/typings/servers.d.ts +8 -1
  88. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  89. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  90. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  91. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  92. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -0
  93. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  94. package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +2 -2
  95. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +10 -0
  96. package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +6 -0
  97. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  98. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  99. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  100. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
  101. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
  102. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +38 -0
  103. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +2 -1
  104. package/esm/typings/src/commitments/index.d.ts +22 -1
  105. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  106. package/esm/typings/src/llm-providers/agent/Agent.d.ts +1 -0
  107. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  108. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  109. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  110. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  111. package/esm/typings/src/version.d.ts +1 -1
  112. package/package.json +1 -1
  113. package/umd/index.umd.js +344 -11
  114. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,58 @@
1
+ // Client Component for rendering and deleting agents
2
+ 'use client';
3
+
4
+ import React, { useState } from 'react';
5
+ import { TrashIcon } from 'lucide-react';
6
+ import Link from 'next/link';
7
+ import { AddAgentButton } from '../../app/AddAgentButton';
8
+ import { AgentCard } from './AgentCard';
9
+ import { Section } from './Section';
10
+
11
+ import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
12
+
13
+ type AgentsListProps = {
14
+ agents: AgentBasicInformation[];
15
+ isAdmin: boolean;
16
+ };
17
+
18
+ export function AgentsList({ agents: initialAgents, isAdmin }: AgentsListProps) {
19
+ const [agents, setAgents] = useState(Array.from(initialAgents));
20
+
21
+ const handleDelete = async (agentName: string) => {
22
+ if (!window.confirm(`Delete agent "${agentName}"? It will be moved to Recycle Bin.`)) return;
23
+ await fetch(`/api/agents/${encodeURIComponent(agentName)}`, { method: 'DELETE' });
24
+ setAgents(agents.filter((a) => a.agentName !== agentName));
25
+ };
26
+
27
+ const handleClone = async (agentName: string) => {
28
+ if (!window.confirm(`Clone agent "${agentName}"?`)) return;
29
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentName)}/clone`, { method: 'POST' });
30
+ const newAgent = await response.json();
31
+ setAgents([...agents, newAgent]);
32
+ };
33
+
34
+ return (
35
+ <Section title={`Agents (${agents.length})`}>
36
+ {agents.map((agent) => (
37
+ <AgentCard
38
+ key={agent.agentName}
39
+ agent={agent}
40
+ href={`/${agent.agentName}`}
41
+ isAdmin={isAdmin}
42
+ onDelete={handleDelete}
43
+ onClone={handleClone}
44
+ />
45
+ ))}
46
+ {isAdmin && <AddAgentButton />}
47
+ {isAdmin && (
48
+ <Link
49
+ href="/recycle-bin"
50
+ className="flex items-center gap-2 px-4 py-2 mt-4 text-gray-600 hover:text-red-600 transition-colors"
51
+ >
52
+ <TrashIcon className="w-4 h-4" />
53
+ Open Recycle Bin
54
+ </Link>
55
+ )}
56
+ </Section>
57
+ );
58
+ }
@@ -9,7 +9,7 @@ type CardProps = {
9
9
  export function Card({ children, className = '', style }: CardProps) {
10
10
  return (
11
11
  <div
12
- className={`block p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400 ${className}`}
12
+ className={`block h-full p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200 hover:border-blue-400 ${className}`}
13
13
  style={style}
14
14
  >
15
15
  {children}
@@ -0,0 +1,21 @@
1
+ import type { AgentsByServer } from '../../utils/getFederatedAgents';
2
+ import { AgentCard } from './AgentCard';
3
+ import { Section } from './Section';
4
+
5
+ type ExternalAgentsSectionProps = {
6
+ agentsByServer: AgentsByServer[];
7
+ };
8
+
9
+ export function ExternalAgentsSection({ agentsByServer }: ExternalAgentsSectionProps) {
10
+ return (
11
+ <>
12
+ {agentsByServer.map(({ serverUrl, agents }) => (
13
+ <Section key={serverUrl} title={`Agents from ${new URL(serverUrl).hostname} (${agents.length})`}>
14
+ {agents.map((agent) => (
15
+ <AgentCard key={agent.url} agent={agent} href={agent.url} />
16
+ ))}
17
+ </Section>
18
+ ))}
19
+ </>
20
+ );
21
+ }
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import type { AgentsByServer } from '../../utils/getFederatedAgents';
5
+ import { AgentCard } from './AgentCard';
6
+ import { Section } from './Section';
7
+
8
+ type FederatedServersResponse = {
9
+ federatedServers: string[];
10
+ };
11
+
12
+ type ServerState = {
13
+ status: 'loading' | 'success' | 'error';
14
+ agents: AgentsByServer['agents'];
15
+ error?: string;
16
+ };
17
+
18
+ export function ExternalAgentsSectionClient() {
19
+ const [servers, setServers] = useState<Record<string, ServerState>>({});
20
+ const [initialLoading, setInitialLoading] = useState(true);
21
+
22
+ useEffect(() => {
23
+ let isCancelled = false;
24
+
25
+ const fetchServers = async () => {
26
+ try {
27
+ const response = await fetch('/api/federated-agents');
28
+ if (!response.ok) throw new Error('Failed to fetch federated servers');
29
+
30
+ const data: FederatedServersResponse = await response.json();
31
+
32
+ if (isCancelled) return;
33
+
34
+ const initialServerState: Record<string, ServerState> = {};
35
+ data.federatedServers.forEach(serverUrl => {
36
+ initialServerState[serverUrl] = { status: 'loading', agents: [] };
37
+ });
38
+
39
+ setServers(initialServerState);
40
+ setInitialLoading(false);
41
+
42
+ // Fetch agents for each server independently
43
+ data.federatedServers.forEach(serverUrl => {
44
+ fetchAgentsForServer(serverUrl);
45
+ });
46
+
47
+ } catch (error) {
48
+ console.error('Failed to load federated servers list', error);
49
+ if (!isCancelled) setInitialLoading(false);
50
+ }
51
+ };
52
+
53
+ const fetchAgentsForServer = async (serverUrl: string) => {
54
+ const normalizedUrl = serverUrl.replace(/\/$/, '');
55
+
56
+ try {
57
+ // 1. Try direct connection
58
+ const response = await fetch(`${normalizedUrl}/api/agents`);
59
+
60
+ if (!response.ok) {
61
+ throw new Error(`Failed to fetch agents from ${serverUrl} (Status: ${response.status})`);
62
+ }
63
+
64
+ const data = await response.json();
65
+
66
+ if (isCancelled) return;
67
+
68
+ setServers((prev) => ({
69
+ ...prev,
70
+ [serverUrl]: {
71
+ status: 'success',
72
+ agents: data.agents || [],
73
+ },
74
+ }));
75
+ } catch (directError) {
76
+ // 2. Try proxy through our server
77
+ try {
78
+ // Note: We are using encodeURIComponent to ensure the URL is passed correctly as a parameter
79
+ const proxyUrl = `/agents/${encodeURIComponent(normalizedUrl)}/api/agents`;
80
+ const response = await fetch(proxyUrl);
81
+
82
+ if (!response.ok) {
83
+ throw new Error(`Failed to fetch agents from ${serverUrl} via proxy (Status: ${response.status})`);
84
+ }
85
+
86
+ const data = await response.json();
87
+
88
+ if (isCancelled) return;
89
+
90
+ setServers((prev) => ({
91
+ ...prev,
92
+ [serverUrl]: {
93
+ status: 'success',
94
+ agents: data.agents || [],
95
+ },
96
+ }));
97
+ } catch (proxyError) {
98
+ if (isCancelled) return;
99
+ console.warn(`Failed to load agents from ${serverUrl} (Direct & Proxy)`, directError, proxyError);
100
+
101
+ setServers((prev) => ({
102
+ ...prev,
103
+ [serverUrl]: {
104
+ status: 'error',
105
+ agents: [],
106
+ error: proxyError instanceof Error ? proxyError.message : 'Unknown error',
107
+ },
108
+ }));
109
+ }
110
+ }
111
+ };
112
+
113
+ fetchServers();
114
+
115
+ return () => {
116
+ isCancelled = true;
117
+ };
118
+ }, []);
119
+
120
+ if (initialLoading) {
121
+ return (
122
+ <div className="mt-8 flex items-center justify-center py-8 text-sm text-gray-500">
123
+ <span className="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
124
+ Loading federated agents…
125
+ </div>
126
+ );
127
+ }
128
+
129
+ const serverUrls = Object.keys(servers);
130
+
131
+ if (serverUrls.length === 0) {
132
+ return null;
133
+ }
134
+
135
+ return (
136
+ <>
137
+ {serverUrls.map(serverUrl => {
138
+ const state = servers[serverUrl];
139
+ const hostname = (() => {
140
+ try {
141
+ return new URL(serverUrl).hostname;
142
+ } catch {
143
+ return serverUrl;
144
+ }
145
+ })();
146
+
147
+ if (state.status === 'loading') {
148
+ return (
149
+ <Section key={serverUrl} title={`Agents from ${hostname} (...)`}>
150
+ <div className="flex items-center justify-center py-8 text-sm text-gray-500">
151
+ <span className="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
152
+ Loading...
153
+ </div>
154
+ </Section>
155
+ );
156
+ }
157
+
158
+ if (state.status === 'error') {
159
+ return (
160
+ <Section key={serverUrl} title={`Agents from ${hostname} (Error)`}>
161
+ <div className="py-4 text-sm text-red-500 text-center">
162
+ Failed to load agents from this server.
163
+ </div>
164
+ </Section>
165
+ );
166
+ }
167
+
168
+ if (state.status === 'success' && state.agents.length > 0) {
169
+ return (
170
+ <Section key={serverUrl} title={`Agents from ${hostname} (${state.agents.length})`}>
171
+ {state.agents.map((agent) => (
172
+ <AgentCard key={agent.url} agent={agent} href={agent.url} />
173
+ ))}
174
+ </Section>
175
+ );
176
+ }
177
+
178
+ // Hide sections with no agents if successfully loaded
179
+ return null;
180
+ })}
181
+ </>
182
+ );
183
+ }
@@ -0,0 +1,75 @@
1
+ import Link from 'next/link';
2
+ import React from 'react';
3
+ import { Section } from './Section';
4
+ import { ModelCard } from './ModelCard';
5
+
6
+ type ModelInfo = {
7
+ modelName: string;
8
+ modelTitle?: string;
9
+ modelDescription?: string;
10
+ };
11
+
12
+ type ModelsSectionProps = {
13
+ /**
14
+ * Full list of models to display.
15
+ */
16
+ models: ReadonlyArray<ModelInfo>;
17
+
18
+ /**
19
+ * Optional custom title for the section.
20
+ * Defaults to `Models (X)` where X is the total number of models.
21
+ */
22
+ title?: string;
23
+
24
+ /**
25
+ * Optional maximum number of models to render.
26
+ * If set and there are more models than this number, only the first `maxVisible`
27
+ * models are shown.
28
+ */
29
+ maxVisible?: number;
30
+
31
+ /**
32
+ * When true and `maxVisible` is set and exceeded, renders a "View all models"
33
+ * card linking to the admin models page.
34
+ */
35
+ showViewAllLink?: boolean;
36
+ };
37
+
38
+ export function ModelsSection(props: ModelsSectionProps) {
39
+ const { models, title, maxVisible, showViewAllLink } = props;
40
+
41
+ const totalCount = models.length;
42
+ const visibleModels =
43
+ typeof maxVisible === 'number'
44
+ ? models.slice(0, Math.max(0, maxVisible))
45
+ : models;
46
+
47
+ const resolvedTitle = title ?? `Models (${totalCount})`;
48
+
49
+ const hasMore =
50
+ typeof maxVisible === 'number' ? totalCount > maxVisible : false;
51
+
52
+ return (
53
+ <Section title={resolvedTitle}>
54
+ {visibleModels.map(({ modelName, modelTitle, modelDescription }) => (
55
+ <ModelCard
56
+ key={modelName}
57
+ modelName={modelName}
58
+ modelTitle={modelTitle || modelName}
59
+ modelDescription={modelDescription}
60
+ />
61
+ ))}
62
+
63
+ {showViewAllLink && hasMore && (
64
+ <Link href="/admin/models">
65
+ <div className="h-full flex flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white/60 text-center p-4 hover:border-promptbook-blue-dark hover:bg-white transition-colors cursor-pointer">
66
+ <p className="text-sm font-medium text-gray-900">View all models</p>
67
+ <p className="text-xs text-gray-500 mt-1">
68
+ Showing {visibleModels.length} of {totalCount} models
69
+ </p>
70
+ </div>
71
+ </Link>
72
+ )}
73
+ </Section>
74
+ );
75
+ }
@@ -1,18 +1,36 @@
1
1
  'use client';
2
2
 
3
3
  import { usePathname } from 'next/navigation';
4
+ import { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
5
+ import type { UserInfo } from '../../utils/getCurrentUser';
6
+ import { Footer, type FooterLink } from '../Footer/Footer';
4
7
  import { Header } from '../Header/Header';
5
8
 
6
9
  type LayoutWrapperProps = {
7
10
  children: React.ReactNode;
8
11
  isAdmin: boolean;
12
+ currentUser: UserInfo | null;
9
13
  serverName: string;
10
14
  serverLogoUrl: string | null;
15
+ agents: Array<AgentBasicInformation>;
16
+ isFooterShown: boolean;
17
+ footerLinks: Array<FooterLink>;
11
18
  };
12
19
 
13
- export function LayoutWrapper({ children, isAdmin, serverName, serverLogoUrl }: LayoutWrapperProps) {
20
+ export function LayoutWrapper({
21
+ children,
22
+ isAdmin,
23
+ currentUser,
24
+ serverName,
25
+ serverLogoUrl,
26
+ agents,
27
+ isFooterShown,
28
+ footerLinks,
29
+ }: LayoutWrapperProps) {
14
30
  const pathname = usePathname();
15
- const isHeaderHidden = pathname?.includes('/chat');
31
+ const isAdminChatPage =
32
+ pathname?.startsWith('/admin/chat-history') || pathname?.startsWith('/admin/chat-feedback');
33
+ const isHeaderHidden = pathname?.includes('/chat') && !isAdminChatPage;
16
34
 
17
35
  if (isHeaderHidden) {
18
36
  return <main className={`pt-0`}>{children}</main>;
@@ -20,8 +38,15 @@ export function LayoutWrapper({ children, isAdmin, serverName, serverLogoUrl }:
20
38
 
21
39
  return (
22
40
  <>
23
- <Header isAdmin={isAdmin} serverName={serverName} serverLogoUrl={serverLogoUrl} />
41
+ <Header
42
+ isAdmin={isAdmin}
43
+ currentUser={currentUser}
44
+ serverName={serverName}
45
+ serverLogoUrl={serverLogoUrl}
46
+ agents={agents}
47
+ />
24
48
  <main className={`pt-[60px]`}>{children}</main>
49
+ {isFooterShown && <Footer extraLinks={footerLinks} />}
25
50
  </>
26
51
  );
27
52
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { X } from 'lucide-react';
4
4
  import { LoginForm } from '../LoginForm/LoginForm';
5
+ import { Portal } from '../Portal/Portal';
5
6
 
6
7
  type LoginDialogProps = {
7
8
  isOpen: boolean;
@@ -16,25 +17,25 @@ export function LoginDialog(props: LoginDialogProps) {
16
17
  }
17
18
 
18
19
  return (
19
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
20
- <div className="relative w-full max-w-md bg-white rounded-lg shadow-lg border border-gray-200 p-6 animate-in zoom-in-95 duration-200">
21
- <button
22
- onClick={onClose}
23
- className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 transition-colors"
24
- >
25
- <X className="w-5 h-5" />
26
- <span className="sr-only">Close</span>
27
- </button>
20
+ <Portal>
21
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
22
+ <div className="relative w-full max-w-md bg-white rounded-lg shadow-lg border border-gray-200 p-6 animate-in zoom-in-95 duration-200">
23
+ <button
24
+ onClick={onClose}
25
+ className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 transition-colors"
26
+ >
27
+ <X className="w-5 h-5" />
28
+ <span className="sr-only">Close</span>
29
+ </button>
28
30
 
29
- <div className="mb-6">
30
- <h2 className="text-xl font-semibold text-gray-900">Log in</h2>
31
- <p className="text-sm text-gray-500 mt-1">
32
- Enter your credentials to access the admin area
33
- </p>
34
- </div>
31
+ <div className="mb-6">
32
+ <h2 className="text-xl font-semibold text-gray-900">Log in</h2>
33
+ <p className="text-sm text-gray-500 mt-1">Enter your credentials to access the admin area</p>
34
+ </div>
35
35
 
36
- <LoginForm onSuccess={onClose} />
36
+ <LoginForm onSuccess={onClose} />
37
+ </div>
37
38
  </div>
38
- </div>
39
+ </Portal>
39
40
  );
40
41
  }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { useEffect, useState } from 'react';
5
+ import { createPortal } from 'react-dom';
6
+
7
+ type PortalProps = {
8
+ children: ReactNode;
9
+ /**
10
+ * Optional container id. Defaults to "portal-root".
11
+ * This allows reusing the Portal component for different layers if needed.
12
+ */
13
+ containerId?: string;
14
+ };
15
+
16
+ /**
17
+ * Generic portal component that renders children into a DOM node
18
+ * outside of the normal React tree (by default #portal-root).
19
+ *
20
+ * This is useful for modals/popups that should visually overlay the
21
+ * whole app, independent of where the triggering component lives.
22
+ */
23
+ export function Portal(props: PortalProps) {
24
+ const { children, containerId = 'portal-root' } = props;
25
+ const [container, setContainer] = useState<Element | null>(null);
26
+
27
+ useEffect(() => {
28
+ const element = document.getElementById(containerId);
29
+ setContainer(element);
30
+ }, [containerId]);
31
+
32
+ if (!container) {
33
+ // In SSR or before the DOM node exists, render nothing.
34
+ return null;
35
+ }
36
+
37
+ return createPortal(children, container);
38
+ }