apteva 0.4.10 → 0.4.11
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.
package/dist/index.html
CHANGED
package/package.json
CHANGED
package/src/mcp-platform.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Built-in MCP server that exposes the Apteva platform API as MCP tools
|
|
2
2
|
// This allows the meta agent (Apteva Assistant) to control the platform
|
|
3
3
|
|
|
4
|
-
import { AgentDB, ProjectDB, McpServerDB, TelemetryDB, generateId } from "./db";
|
|
4
|
+
import { AgentDB, ProjectDB, McpServerDB, SkillDB, TelemetryDB, generateId } from "./db";
|
|
5
5
|
import { getProvidersWithStatus, PROVIDERS } from "./providers";
|
|
6
6
|
import { startAgentProcess, setAgentStatus, toApiAgent, META_AGENT_ID, agentFetch } from "./routes/api/agent-utils";
|
|
7
7
|
import { agentProcesses } from "./server";
|
|
@@ -153,10 +153,76 @@ const PLATFORM_TOOLS = [
|
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
name: "list_mcp_servers",
|
|
156
|
-
description: "List all configured MCP servers (tool integrations).",
|
|
156
|
+
description: "List all configured MCP servers (tool integrations). Optionally filter by project.",
|
|
157
157
|
inputSchema: {
|
|
158
158
|
type: "object",
|
|
159
|
-
properties: {
|
|
159
|
+
properties: {
|
|
160
|
+
project_id: { type: "string", description: "Filter by project ID (optional)" },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "get_mcp_server",
|
|
166
|
+
description: "Get detailed information about an MCP server by ID.",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
server_id: { type: "string", description: "The MCP server ID" },
|
|
171
|
+
},
|
|
172
|
+
required: ["server_id"],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "create_mcp_server",
|
|
177
|
+
description: "Create a new MCP server. For HTTP (remote) servers, provide url and optional headers. For npm package servers, provide a package name.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
name: { type: "string", description: "Server display name" },
|
|
182
|
+
type: { type: "string", description: "Server type: 'http' (remote URL), 'npm' (npm package), 'pip' (Python package), 'custom' (custom command)" },
|
|
183
|
+
url: { type: "string", description: "For http type: the remote MCP server URL" },
|
|
184
|
+
headers: { type: "object", description: "For http type: auth headers (e.g. {\"Authorization\": \"Bearer ...\"})" },
|
|
185
|
+
package: { type: "string", description: "For npm/pip type: the package name (e.g. '@modelcontextprotocol/server-filesystem')" },
|
|
186
|
+
command: { type: "string", description: "For custom type: the command to run" },
|
|
187
|
+
args: { type: "string", description: "Command arguments (optional)" },
|
|
188
|
+
project_id: { type: "string", description: "Project ID to scope the server to (optional, null = global)" },
|
|
189
|
+
},
|
|
190
|
+
required: ["name", "type"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "delete_mcp_server",
|
|
195
|
+
description: "Delete an MCP server. It must be stopped first.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
server_id: { type: "string", description: "The MCP server ID to delete" },
|
|
200
|
+
},
|
|
201
|
+
required: ["server_id"],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "assign_mcp_server_to_agent",
|
|
206
|
+
description: "Assign an MCP server to an agent so the agent can use its tools. The agent must have MCP feature enabled.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
agent_id: { type: "string", description: "The agent ID" },
|
|
211
|
+
server_id: { type: "string", description: "The MCP server ID to assign" },
|
|
212
|
+
},
|
|
213
|
+
required: ["agent_id", "server_id"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "unassign_mcp_server_from_agent",
|
|
218
|
+
description: "Remove an MCP server from an agent.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
agent_id: { type: "string", description: "The agent ID" },
|
|
223
|
+
server_id: { type: "string", description: "The MCP server ID to remove" },
|
|
224
|
+
},
|
|
225
|
+
required: ["agent_id", "server_id"],
|
|
160
226
|
},
|
|
161
227
|
},
|
|
162
228
|
{
|
|
@@ -179,6 +245,75 @@ const PLATFORM_TOOLS = [
|
|
|
179
245
|
required: ["agent_id", "message"],
|
|
180
246
|
},
|
|
181
247
|
},
|
|
248
|
+
// Skills management
|
|
249
|
+
{
|
|
250
|
+
name: "list_skills",
|
|
251
|
+
description: "List all installed skills. Skills are reusable instruction sets that give agents specialized capabilities.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
enabled_only: { type: "boolean", description: "Only return enabled skills (optional, default false)" },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: "get_skill",
|
|
261
|
+
description: "Get detailed information about a skill by ID, including its full instructions content.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
skill_id: { type: "string", description: "The skill ID" },
|
|
266
|
+
},
|
|
267
|
+
required: ["skill_id"],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "toggle_skill",
|
|
272
|
+
description: "Enable or disable a skill.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
skill_id: { type: "string", description: "The skill ID" },
|
|
277
|
+
enabled: { type: "boolean", description: "Whether to enable (true) or disable (false) the skill" },
|
|
278
|
+
},
|
|
279
|
+
required: ["skill_id", "enabled"],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "assign_skill_to_agent",
|
|
284
|
+
description: "Assign a skill to an agent so it can use those instructions.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
agent_id: { type: "string", description: "The agent ID" },
|
|
289
|
+
skill_id: { type: "string", description: "The skill ID to assign" },
|
|
290
|
+
},
|
|
291
|
+
required: ["agent_id", "skill_id"],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "unassign_skill_from_agent",
|
|
296
|
+
description: "Remove a skill from an agent.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
agent_id: { type: "string", description: "The agent ID" },
|
|
301
|
+
skill_id: { type: "string", description: "The skill ID to remove" },
|
|
302
|
+
},
|
|
303
|
+
required: ["agent_id", "skill_id"],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "delete_skill",
|
|
308
|
+
description: "Delete a skill. It will be unassigned from all agents.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
skill_id: { type: "string", description: "The skill ID to delete" },
|
|
313
|
+
},
|
|
314
|
+
required: ["skill_id"],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
182
317
|
];
|
|
183
318
|
|
|
184
319
|
// Tool execution handlers
|
|
@@ -351,24 +486,114 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
|
|
|
351
486
|
}
|
|
352
487
|
|
|
353
488
|
case "list_mcp_servers": {
|
|
354
|
-
const servers =
|
|
489
|
+
const servers = args.project_id
|
|
490
|
+
? McpServerDB.findByProject(args.project_id)
|
|
491
|
+
: McpServerDB.findAll();
|
|
355
492
|
const result = servers.map(s => ({
|
|
356
493
|
id: s.id,
|
|
357
494
|
name: s.name,
|
|
358
495
|
type: s.type,
|
|
359
496
|
status: s.status,
|
|
360
497
|
url: s.url,
|
|
498
|
+
package: s.package,
|
|
499
|
+
projectId: s.project_id,
|
|
361
500
|
}));
|
|
362
501
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
363
502
|
}
|
|
364
503
|
|
|
504
|
+
case "get_mcp_server": {
|
|
505
|
+
const server = McpServerDB.findById(args.server_id);
|
|
506
|
+
if (!server) {
|
|
507
|
+
return { content: [{ type: "text", text: `MCP server not found: ${args.server_id}` }], isError: true };
|
|
508
|
+
}
|
|
509
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
510
|
+
id: server.id,
|
|
511
|
+
name: server.name,
|
|
512
|
+
type: server.type,
|
|
513
|
+
status: server.status,
|
|
514
|
+
url: server.url,
|
|
515
|
+
package: server.package,
|
|
516
|
+
command: server.command,
|
|
517
|
+
args: server.args,
|
|
518
|
+
port: server.port,
|
|
519
|
+
source: server.source,
|
|
520
|
+
projectId: server.project_id,
|
|
521
|
+
}, null, 2) }] };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
case "create_mcp_server": {
|
|
525
|
+
const id = generateId();
|
|
526
|
+
const server = McpServerDB.create({
|
|
527
|
+
id,
|
|
528
|
+
name: args.name,
|
|
529
|
+
type: args.type || "http",
|
|
530
|
+
package: args.package || null,
|
|
531
|
+
pip_module: null,
|
|
532
|
+
command: args.command || null,
|
|
533
|
+
args: args.args || null,
|
|
534
|
+
env: {},
|
|
535
|
+
url: args.url || null,
|
|
536
|
+
headers: args.headers || {},
|
|
537
|
+
source: null,
|
|
538
|
+
project_id: args.project_id || null,
|
|
539
|
+
});
|
|
540
|
+
return { content: [{ type: "text", text: `MCP server created: ${JSON.stringify({ id: server.id, name: server.name, type: server.type }, null, 2)}` }] };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case "delete_mcp_server": {
|
|
544
|
+
const server = McpServerDB.findById(args.server_id);
|
|
545
|
+
if (!server) {
|
|
546
|
+
return { content: [{ type: "text", text: `MCP server not found: ${args.server_id}` }], isError: true };
|
|
547
|
+
}
|
|
548
|
+
if (server.status === "running") {
|
|
549
|
+
return { content: [{ type: "text", text: "Cannot delete a running MCP server. Stop it first." }], isError: true };
|
|
550
|
+
}
|
|
551
|
+
McpServerDB.delete(args.server_id);
|
|
552
|
+
return { content: [{ type: "text", text: `MCP server deleted: ${server.name} (${server.id})` }] };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case "assign_mcp_server_to_agent": {
|
|
556
|
+
const agent = AgentDB.findById(args.agent_id);
|
|
557
|
+
if (!agent) {
|
|
558
|
+
return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
|
|
559
|
+
}
|
|
560
|
+
const server = McpServerDB.findById(args.server_id);
|
|
561
|
+
if (!server) {
|
|
562
|
+
return { content: [{ type: "text", text: `MCP server not found: ${args.server_id}` }], isError: true };
|
|
563
|
+
}
|
|
564
|
+
const mcpServers = agent.mcp_servers || [];
|
|
565
|
+
if (mcpServers.includes(args.server_id)) {
|
|
566
|
+
return { content: [{ type: "text", text: `Server ${server.name} is already assigned to ${agent.name}` }] };
|
|
567
|
+
}
|
|
568
|
+
AgentDB.update(args.agent_id, { mcp_servers: [...mcpServers, args.server_id] });
|
|
569
|
+
// Enable MCP feature if not already
|
|
570
|
+
if (!agent.features.mcp) {
|
|
571
|
+
AgentDB.update(args.agent_id, { features: { ...agent.features, mcp: true } });
|
|
572
|
+
}
|
|
573
|
+
return { content: [{ type: "text", text: `Assigned MCP server "${server.name}" to agent "${agent.name}". Restart the agent for changes to take effect.` }] };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
case "unassign_mcp_server_from_agent": {
|
|
577
|
+
const agent = AgentDB.findById(args.agent_id);
|
|
578
|
+
if (!agent) {
|
|
579
|
+
return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
|
|
580
|
+
}
|
|
581
|
+
const mcpServers = agent.mcp_servers || [];
|
|
582
|
+
if (!mcpServers.includes(args.server_id)) {
|
|
583
|
+
return { content: [{ type: "text", text: `Server is not assigned to this agent` }] };
|
|
584
|
+
}
|
|
585
|
+
AgentDB.update(args.agent_id, { mcp_servers: mcpServers.filter((id: string) => id !== args.server_id) });
|
|
586
|
+
return { content: [{ type: "text", text: `Removed MCP server from agent "${agent.name}". Restart the agent for changes to take effect.` }] };
|
|
587
|
+
}
|
|
588
|
+
|
|
365
589
|
case "get_dashboard_stats": {
|
|
366
590
|
const agentCount = AgentDB.count();
|
|
367
591
|
const runningCount = AgentDB.countRunning();
|
|
368
592
|
const projectCount = ProjectDB.count();
|
|
369
593
|
const providers = getProvidersWithStatus().filter(p => p.type === "llm");
|
|
370
594
|
const configuredProviders = providers.filter(p => p.hasKey).length;
|
|
371
|
-
const
|
|
595
|
+
const mcpServerCount = McpServerDB.findAll().length;
|
|
596
|
+
const skillCount = SkillDB.count();
|
|
372
597
|
|
|
373
598
|
return {
|
|
374
599
|
content: [{
|
|
@@ -377,7 +602,8 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
|
|
|
377
602
|
agents: { total: agentCount - 1, running: runningCount }, // -1 for meta agent
|
|
378
603
|
projects: projectCount,
|
|
379
604
|
providers: { total: providers.length, configured: configuredProviders },
|
|
380
|
-
mcpServers,
|
|
605
|
+
mcpServers: mcpServerCount,
|
|
606
|
+
skills: skillCount,
|
|
381
607
|
}, null, 2),
|
|
382
608
|
}],
|
|
383
609
|
};
|
|
@@ -413,6 +639,92 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
|
|
|
413
639
|
}
|
|
414
640
|
}
|
|
415
641
|
|
|
642
|
+
case "list_skills": {
|
|
643
|
+
const skills = args.enabled_only ? SkillDB.findEnabled() : SkillDB.findAll();
|
|
644
|
+
const result = skills.map(s => ({
|
|
645
|
+
id: s.id,
|
|
646
|
+
name: s.name,
|
|
647
|
+
description: s.description,
|
|
648
|
+
version: s.version,
|
|
649
|
+
enabled: s.enabled,
|
|
650
|
+
source: s.source,
|
|
651
|
+
projectId: s.project_id,
|
|
652
|
+
}));
|
|
653
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
case "get_skill": {
|
|
657
|
+
const skill = SkillDB.findById(args.skill_id);
|
|
658
|
+
if (!skill) {
|
|
659
|
+
return { content: [{ type: "text", text: `Skill not found: ${args.skill_id}` }], isError: true };
|
|
660
|
+
}
|
|
661
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
662
|
+
id: skill.id,
|
|
663
|
+
name: skill.name,
|
|
664
|
+
description: skill.description,
|
|
665
|
+
content: skill.content.slice(0, 500) + (skill.content.length > 500 ? "..." : ""),
|
|
666
|
+
version: skill.version,
|
|
667
|
+
enabled: skill.enabled,
|
|
668
|
+
source: skill.source,
|
|
669
|
+
allowedTools: skill.allowed_tools,
|
|
670
|
+
projectId: skill.project_id,
|
|
671
|
+
}, null, 2) }] };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
case "toggle_skill": {
|
|
675
|
+
const skill = SkillDB.findById(args.skill_id);
|
|
676
|
+
if (!skill) {
|
|
677
|
+
return { content: [{ type: "text", text: `Skill not found: ${args.skill_id}` }], isError: true };
|
|
678
|
+
}
|
|
679
|
+
SkillDB.setEnabled(args.skill_id, args.enabled);
|
|
680
|
+
return { content: [{ type: "text", text: `Skill "${skill.name}" ${args.enabled ? "enabled" : "disabled"}` }] };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
case "assign_skill_to_agent": {
|
|
684
|
+
const agent = AgentDB.findById(args.agent_id);
|
|
685
|
+
if (!agent) {
|
|
686
|
+
return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
|
|
687
|
+
}
|
|
688
|
+
const skill = SkillDB.findById(args.skill_id);
|
|
689
|
+
if (!skill) {
|
|
690
|
+
return { content: [{ type: "text", text: `Skill not found: ${args.skill_id}` }], isError: true };
|
|
691
|
+
}
|
|
692
|
+
const skills = agent.skills || [];
|
|
693
|
+
if (skills.includes(args.skill_id)) {
|
|
694
|
+
return { content: [{ type: "text", text: `Skill "${skill.name}" is already assigned to "${agent.name}"` }] };
|
|
695
|
+
}
|
|
696
|
+
AgentDB.update(args.agent_id, { skills: [...skills, args.skill_id] });
|
|
697
|
+
return { content: [{ type: "text", text: `Assigned skill "${skill.name}" to agent "${agent.name}". Restart the agent for changes to take effect.` }] };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
case "unassign_skill_from_agent": {
|
|
701
|
+
const agent = AgentDB.findById(args.agent_id);
|
|
702
|
+
if (!agent) {
|
|
703
|
+
return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
|
|
704
|
+
}
|
|
705
|
+
const skills = agent.skills || [];
|
|
706
|
+
if (!skills.includes(args.skill_id)) {
|
|
707
|
+
return { content: [{ type: "text", text: `Skill is not assigned to this agent` }] };
|
|
708
|
+
}
|
|
709
|
+
AgentDB.update(args.agent_id, { skills: skills.filter((id: string) => id !== args.skill_id) });
|
|
710
|
+
return { content: [{ type: "text", text: `Removed skill from agent "${agent.name}". Restart the agent for changes to take effect.` }] };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
case "delete_skill": {
|
|
714
|
+
const skill = SkillDB.findById(args.skill_id);
|
|
715
|
+
if (!skill) {
|
|
716
|
+
return { content: [{ type: "text", text: `Skill not found: ${args.skill_id}` }], isError: true };
|
|
717
|
+
}
|
|
718
|
+
// Unassign from all agents first
|
|
719
|
+
const agentsWithSkill = AgentDB.findBySkill(args.skill_id);
|
|
720
|
+
for (const agent of agentsWithSkill) {
|
|
721
|
+
const updated = (agent.skills || []).filter((id: string) => id !== args.skill_id);
|
|
722
|
+
AgentDB.update(agent.id, { skills: updated });
|
|
723
|
+
}
|
|
724
|
+
SkillDB.delete(args.skill_id);
|
|
725
|
+
return { content: [{ type: "text", text: `Skill "${skill.name}" deleted${agentsWithSkill.length > 0 ? ` (unassigned from ${agentsWithSkill.length} agent(s))` : ""}` }] };
|
|
726
|
+
}
|
|
727
|
+
|
|
416
728
|
default:
|
|
417
729
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
418
730
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useState, useEffect, createContext, useContext, type ReactNode } from "react";
|
|
1
|
+
import React, { useState, useEffect, useMemo, createContext, useContext, type ReactNode } from "react";
|
|
2
2
|
import { Chat } from "@apteva/apteva-kit";
|
|
3
|
-
import { useAuth } from "../../context";
|
|
3
|
+
import { useAuth, useProjects } from "../../context";
|
|
4
4
|
|
|
5
5
|
interface MetaAgentStatus {
|
|
6
6
|
enabled: boolean;
|
|
@@ -129,10 +129,24 @@ export function MetaAgentButton() {
|
|
|
129
129
|
// Chat panel component - renders as a right-side drawer
|
|
130
130
|
export function MetaAgentPanel() {
|
|
131
131
|
const ctx = useMetaAgent();
|
|
132
|
+
const { currentProjectId, currentProject } = useProjects();
|
|
132
133
|
if (!ctx?.isAvailable || !ctx.isOpen) return null;
|
|
133
134
|
|
|
134
135
|
const { agent, isRunning, error, isStarting, startAgent, close } = ctx;
|
|
135
136
|
|
|
137
|
+
// Build context string for the meta agent
|
|
138
|
+
const chatContext = useMemo(() => {
|
|
139
|
+
const parts: string[] = [];
|
|
140
|
+
if (currentProject) {
|
|
141
|
+
parts.push(`Current project: "${currentProject.name}" (id: ${currentProject.id})`);
|
|
142
|
+
} else if (currentProjectId === "unassigned") {
|
|
143
|
+
parts.push("Viewing: unassigned agents (no project)");
|
|
144
|
+
} else {
|
|
145
|
+
parts.push("Viewing: All Projects");
|
|
146
|
+
}
|
|
147
|
+
return parts.join("\n");
|
|
148
|
+
}, [currentProjectId, currentProject]);
|
|
149
|
+
|
|
136
150
|
return (
|
|
137
151
|
<>
|
|
138
152
|
{/* Backdrop */}
|
|
@@ -166,6 +180,7 @@ export function MetaAgentPanel() {
|
|
|
166
180
|
placeholder="Ask me anything about Apteva..."
|
|
167
181
|
variant="terminal"
|
|
168
182
|
showHeader={false}
|
|
183
|
+
context={chatContext}
|
|
169
184
|
/>
|
|
170
185
|
) : (
|
|
171
186
|
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
|