apteva 0.2.7 → 0.2.8

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 (40) hide show
  1. package/dist/App.hzbfeg94.js +217 -0
  2. package/dist/index.html +3 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +561 -32
  9. package/src/routes/api.ts +901 -35
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +46 -5
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +24 -22
  14. package/src/web/components/agents/AgentPanel.tsx +751 -11
  15. package/src/web/components/agents/AgentsView.tsx +81 -9
  16. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  17. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  18. package/src/web/components/auth/LoginPage.tsx +91 -0
  19. package/src/web/components/auth/index.ts +2 -0
  20. package/src/web/components/common/Icons.tsx +48 -0
  21. package/src/web/components/common/Modal.tsx +1 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +70 -22
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +135 -18
  25. package/src/web/components/layout/Sidebar.tsx +81 -43
  26. package/src/web/components/mcp/McpPage.tsx +261 -32
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  28. package/src/web/components/settings/SettingsPage.tsx +320 -21
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/index.ts +5 -0
  34. package/src/web/hooks/useAgents.ts +18 -6
  35. package/src/web/hooks/useOnboarding.ts +20 -4
  36. package/src/web/hooks/useProviders.ts +15 -5
  37. package/src/web/icon.png +0 -0
  38. package/src/web/styles.css +12 -0
  39. package/src/web/types.ts +6 -0
  40. package/dist/App.3kb50qa3.js +0 -213
@@ -1,7 +1,8 @@
1
- import React from "react";
1
+ import React, { useMemo } from "react";
2
2
  import { AgentCard } from "./AgentCard";
3
3
  import { AgentPanel } from "./AgentPanel";
4
4
  import { LoadingSpinner } from "../common/LoadingSpinner";
5
+ import { useProjects } from "../../context";
5
6
  import type { Agent, Provider } from "../../types";
6
7
 
7
8
  interface AgentsViewProps {
@@ -14,6 +15,8 @@ interface AgentsViewProps {
14
15
  onToggleAgent: (agent: Agent, e?: React.MouseEvent) => void;
15
16
  onDeleteAgent: (id: string, e?: React.MouseEvent) => void;
16
17
  onUpdateAgent: (id: string, updates: Partial<Agent>) => Promise<{ error?: string }>;
18
+ onNewAgent?: () => void;
19
+ canCreateAgent?: boolean;
17
20
  }
18
21
 
19
22
  export function AgentsView({
@@ -26,25 +29,76 @@ export function AgentsView({
26
29
  onToggleAgent,
27
30
  onDeleteAgent,
28
31
  onUpdateAgent,
32
+ onNewAgent,
33
+ canCreateAgent = true,
29
34
  }: AgentsViewProps) {
35
+ const { currentProjectId, currentProject } = useProjects();
36
+
37
+ // Filter agents by current project
38
+ const filteredAgents = useMemo(() => {
39
+ if (currentProjectId === null) {
40
+ // "All Projects" - show all agents
41
+ return agents;
42
+ }
43
+ if (currentProjectId === "unassigned") {
44
+ // Show only agents without a project
45
+ return agents.filter(a => !a.projectId);
46
+ }
47
+ // Show only agents in the selected project
48
+ return agents.filter(a => a.projectId === currentProjectId);
49
+ }, [agents, currentProjectId]);
50
+
51
+ const headerTitle = currentProjectId === null
52
+ ? "Agents"
53
+ : currentProjectId === "unassigned"
54
+ ? "Unassigned Agents"
55
+ : currentProject?.name || "Agents";
56
+
30
57
  return (
31
58
  <div className="flex-1 flex overflow-hidden relative">
32
59
  {/* Agents list */}
33
60
  <div className="flex-1 overflow-auto p-6">
61
+ {/* Header with create button */}
62
+ <div className="flex items-center justify-between mb-6">
63
+ <div className="flex items-center gap-3">
64
+ {currentProject && (
65
+ <span
66
+ className="w-3 h-3 rounded-full"
67
+ style={{ backgroundColor: currentProject.color }}
68
+ />
69
+ )}
70
+ <h1 className="text-xl font-semibold">{headerTitle}</h1>
71
+ {currentProjectId !== null && (
72
+ <span className="text-sm text-[#666]">
73
+ ({filteredAgents.length} agent{filteredAgents.length !== 1 ? "s" : ""})
74
+ </span>
75
+ )}
76
+ </div>
77
+ {onNewAgent && (
78
+ <button
79
+ onClick={onNewAgent}
80
+ disabled={!canCreateAgent}
81
+ className="bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-2 rounded font-medium transition"
82
+ >
83
+ + New Agent
84
+ </button>
85
+ )}
86
+ </div>
87
+
34
88
  {loading ? (
35
89
  <LoadingSpinner message="Loading agents..." />
36
- ) : agents.length === 0 ? (
37
- <EmptyState />
90
+ ) : filteredAgents.length === 0 ? (
91
+ <EmptyState onNewAgent={onNewAgent} canCreateAgent={canCreateAgent} hasProjectFilter={currentProjectId !== null} />
38
92
  ) : (
39
93
  <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
40
- {agents.map((agent) => (
94
+ {filteredAgents.map((agent) => (
41
95
  <AgentCard
42
96
  key={agent.id}
43
97
  agent={agent}
44
98
  selected={selectedAgent?.id === agent.id}
45
99
  onSelect={() => onSelectAgent(agent)}
46
100
  onToggle={(e) => onToggleAgent(agent, e)}
47
- onDelete={(e) => onDeleteAgent(agent.id, e)}
101
+ showProject={currentProjectId === null}
48
102
  />
49
103
  ))}
50
104
  </div>
@@ -61,7 +115,7 @@ export function AgentsView({
61
115
 
62
116
  {/* Agent Panel - slides in from right */}
63
117
  {selectedAgent && (
64
- <div className="absolute right-0 top-0 bottom-0 w-[600px] z-20">
118
+ <div className="absolute right-0 top-0 bottom-0 w-full sm:w-[500px] lg:w-[600px] xl:w-[700px] z-20">
65
119
  <AgentPanel
66
120
  agent={selectedAgent}
67
121
  providers={providers}
@@ -76,11 +130,29 @@ export function AgentsView({
76
130
  );
77
131
  }
78
132
 
79
- function EmptyState() {
133
+ function EmptyState({ onNewAgent, canCreateAgent, hasProjectFilter }: { onNewAgent?: () => void; canCreateAgent?: boolean; hasProjectFilter?: boolean }) {
80
134
  return (
81
135
  <div className="text-center py-20 text-[#666]">
82
- <p className="text-lg">No agents yet</p>
83
- <p className="text-sm mt-1">Create your first agent to get started</p>
136
+ {hasProjectFilter ? (
137
+ <>
138
+ <p className="text-lg">No agents in this project</p>
139
+ <p className="text-sm mt-1">Create an agent or assign existing agents to this project</p>
140
+ </>
141
+ ) : (
142
+ <>
143
+ <p className="text-lg">No agents yet</p>
144
+ <p className="text-sm mt-1">Create your first agent to get started</p>
145
+ </>
146
+ )}
147
+ {onNewAgent && (
148
+ <button
149
+ onClick={onNewAgent}
150
+ disabled={!canCreateAgent}
151
+ className="mt-4 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-2 rounded font-medium transition"
152
+ >
153
+ + New Agent
154
+ </button>
155
+ )}
84
156
  </div>
85
157
  );
86
158
  }
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
2
  import { Modal } from "../common/Modal";
3
3
  import { Select } from "../common/Select";
4
- import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon } from "../common/Icons";
4
+ import { MemoryIcon, TasksIcon, FilesIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, MultiAgentIcon } from "../common/Icons";
5
+ import { useProjects } from "../../context";
5
6
  import type { Provider, NewAgentForm, AgentFeatures } from "../../types";
6
7
 
7
8
  interface CreateAgentModalProps {
@@ -18,10 +19,12 @@ interface CreateAgentModalProps {
18
19
  const FEATURE_CONFIG = [
19
20
  { key: "memory" as keyof AgentFeatures, label: "Memory", description: "Persistent recall", icon: MemoryIcon },
20
21
  { key: "tasks" as keyof AgentFeatures, label: "Tasks", description: "Schedule and execute tasks", icon: TasksIcon },
22
+ { key: "files" as keyof AgentFeatures, label: "Files", description: "File storage and management", icon: FilesIcon },
21
23
  { key: "vision" as keyof AgentFeatures, label: "Vision", description: "Process images and PDFs", icon: VisionIcon },
22
24
  { key: "operator" as keyof AgentFeatures, label: "Operator", description: "Browser automation", icon: OperatorIcon },
23
25
  { key: "mcp" as keyof AgentFeatures, label: "MCP", description: "External tools/services", icon: McpIcon },
24
26
  { key: "realtime" as keyof AgentFeatures, label: "Realtime", description: "Voice conversations", icon: RealtimeIcon },
27
+ { key: "agents" as keyof AgentFeatures, label: "Multi-Agent", description: "Communicate with peer agents", icon: MultiAgentIcon },
25
28
  ];
26
29
 
27
30
  export function CreateAgentModal({
@@ -34,6 +37,7 @@ export function CreateAgentModal({
34
37
  onClose,
35
38
  onGoToSettings,
36
39
  }: CreateAgentModalProps) {
40
+ const { projects, currentProjectId } = useProjects();
37
41
  const selectedProvider = providers.find(p => p.id === form.provider);
38
42
 
39
43
  const providerOptions = configuredProviders.map(p => ({
@@ -47,6 +51,18 @@ export function CreateAgentModal({
47
51
  recommended: m.recommended,
48
52
  })) || [];
49
53
 
54
+ const projectOptions = [
55
+ { value: "", label: "No Project" },
56
+ ...projects.map(p => ({ value: p.id, label: p.name })),
57
+ ];
58
+
59
+ // Set default project from current selection (but not "unassigned" or "all")
60
+ React.useEffect(() => {
61
+ if (form.projectId === undefined && currentProjectId && currentProjectId !== "unassigned") {
62
+ onFormChange({ ...form, projectId: currentProjectId });
63
+ }
64
+ }, [currentProjectId]);
65
+
50
66
  const toggleFeature = (key: keyof AgentFeatures) => {
51
67
  onFormChange({
52
68
  ...form,
@@ -76,6 +92,17 @@ export function CreateAgentModal({
76
92
  />
77
93
  </FormField>
78
94
 
95
+ {projects.length > 0 && (
96
+ <FormField label="Project">
97
+ <Select
98
+ value={form.projectId || ""}
99
+ options={projectOptions}
100
+ onChange={(value) => onFormChange({ ...form, projectId: value || null })}
101
+ placeholder="Select project..."
102
+ />
103
+ </FormField>
104
+ )}
105
+
79
106
  <FormField label="Provider">
80
107
  <Select
81
108
  value={form.provider}
@@ -0,0 +1,176 @@
1
+ import React, { useState } from "react";
2
+
3
+ interface CreateAccountStepProps {
4
+ onComplete: (user: { username: string }) => void;
5
+ }
6
+
7
+ export function CreateAccountStep({ onComplete }: CreateAccountStepProps) {
8
+ const [username, setUsername] = useState("");
9
+ const [password, setPassword] = useState("");
10
+ const [confirmPassword, setConfirmPassword] = useState("");
11
+ const [email, setEmail] = useState("");
12
+ const [showEmail, setShowEmail] = useState(false);
13
+ const [error, setError] = useState<string | null>(null);
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ const handleSubmit = async (e: React.FormEvent) => {
17
+ e.preventDefault();
18
+ setError(null);
19
+
20
+ // Validate passwords match
21
+ if (password !== confirmPassword) {
22
+ setError("Passwords do not match");
23
+ return;
24
+ }
25
+
26
+ setLoading(true);
27
+
28
+ try {
29
+ const res = await fetch("/api/onboarding/user", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ username,
34
+ password,
35
+ ...(email && { email }), // Only include if provided
36
+ }),
37
+ });
38
+
39
+ const data = await res.json();
40
+
41
+ if (!res.ok) {
42
+ setError(data.error || "Failed to create account");
43
+ setLoading(false);
44
+ return;
45
+ }
46
+
47
+ // Auto-login after account creation
48
+ const loginRes = await fetch("/api/auth/login", {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ credentials: "include",
52
+ body: JSON.stringify({ username, password }),
53
+ });
54
+
55
+ if (!loginRes.ok) {
56
+ setError("Account created but login failed. Please try logging in.");
57
+ setLoading(false);
58
+ return;
59
+ }
60
+
61
+ const loginData = await loginRes.json();
62
+
63
+ // Store token for subsequent requests
64
+ if (loginData.accessToken) {
65
+ sessionStorage.setItem("accessToken", loginData.accessToken);
66
+ }
67
+
68
+ onComplete({ username });
69
+ } catch (e) {
70
+ setError("Failed to create account");
71
+ setLoading(false);
72
+ }
73
+ };
74
+
75
+ return (
76
+ <>
77
+ <h2 className="text-2xl font-semibold mb-2">Create your account</h2>
78
+ <p className="text-[#666] mb-6">
79
+ Set up your admin account to get started with apteva.
80
+ </p>
81
+
82
+ <form onSubmit={handleSubmit} className="space-y-4">
83
+ <div>
84
+ <label htmlFor="username" className="block text-sm text-[#888] mb-1">
85
+ Username
86
+ </label>
87
+ <input
88
+ id="username"
89
+ type="text"
90
+ value={username}
91
+ onChange={e => setUsername(e.target.value)}
92
+ placeholder="Choose a username"
93
+ autoFocus
94
+ required
95
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
96
+ />
97
+ <p className="text-xs text-[#666] mt-1">3-20 characters, letters, numbers, underscore</p>
98
+ </div>
99
+
100
+ <div>
101
+ <label htmlFor="password" className="block text-sm text-[#888] mb-1">
102
+ Password
103
+ </label>
104
+ <input
105
+ id="password"
106
+ type="password"
107
+ value={password}
108
+ onChange={e => setPassword(e.target.value)}
109
+ placeholder="Enter a password"
110
+ required
111
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
112
+ />
113
+ <p className="text-xs text-[#666] mt-1">Min 8 characters, uppercase, lowercase, number</p>
114
+ </div>
115
+
116
+ <div>
117
+ <label htmlFor="confirmPassword" className="block text-sm text-[#888] mb-1">
118
+ Confirm Password
119
+ </label>
120
+ <input
121
+ id="confirmPassword"
122
+ type="password"
123
+ value={confirmPassword}
124
+ onChange={e => setConfirmPassword(e.target.value)}
125
+ placeholder="Confirm your password"
126
+ required
127
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
128
+ />
129
+ </div>
130
+
131
+ {!showEmail ? (
132
+ <button
133
+ type="button"
134
+ onClick={() => setShowEmail(true)}
135
+ className="text-sm text-[#666] hover:text-[#888] transition"
136
+ >
137
+ + Add email for password recovery (optional)
138
+ </button>
139
+ ) : (
140
+ <div>
141
+ <label htmlFor="email" className="block text-sm text-[#888] mb-1">
142
+ Email <span className="text-[#666]">(optional)</span>
143
+ </label>
144
+ <input
145
+ id="email"
146
+ type="email"
147
+ value={email}
148
+ onChange={e => setEmail(e.target.value)}
149
+ placeholder="For password recovery only"
150
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
151
+ />
152
+ <p className="text-xs text-[#666] mt-1">Only used for password recovery, never shared</p>
153
+ </div>
154
+ )}
155
+
156
+ {error && (
157
+ <div className="p-3 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm">
158
+ {error}
159
+ </div>
160
+ )}
161
+
162
+ <button
163
+ type="submit"
164
+ disabled={loading || !username || !password || !confirmPassword}
165
+ className="w-full bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-3 rounded font-medium transition"
166
+ >
167
+ {loading ? "Creating account..." : "Create Account"}
168
+ </button>
169
+ </form>
170
+
171
+ <p className="text-xs text-[#666] mt-4 text-center">
172
+ This will be your admin account with full access to apteva.
173
+ </p>
174
+ </>
175
+ );
176
+ }
@@ -0,0 +1,91 @@
1
+ import React, { useState } from "react";
2
+ import { useAuth } from "../../context/AuthContext";
3
+
4
+ export function LoginPage() {
5
+ const { login } = useAuth();
6
+ const [username, setUsername] = useState("");
7
+ const [password, setPassword] = useState("");
8
+ const [error, setError] = useState<string | null>(null);
9
+ const [loading, setLoading] = useState(false);
10
+
11
+ const handleSubmit = async (e: React.FormEvent) => {
12
+ e.preventDefault();
13
+ setError(null);
14
+ setLoading(true);
15
+
16
+ const result = await login(username, password);
17
+
18
+ if (!result.success) {
19
+ setError(result.error || "Login failed");
20
+ }
21
+
22
+ setLoading(false);
23
+ };
24
+
25
+ return (
26
+ <div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex items-center justify-center p-8">
27
+ <div className="w-full max-w-md">
28
+ {/* Logo */}
29
+ <div className="text-center mb-8">
30
+ <div className="flex items-center justify-center gap-2 mb-2">
31
+ <span className="text-[#f97316] text-3xl">&gt;_</span>
32
+ <span className="text-3xl tracking-wider">apteva</span>
33
+ </div>
34
+ <p className="text-[#666]">Run AI agents locally</p>
35
+ </div>
36
+
37
+ <div className="bg-[#111] rounded-lg border border-[#1a1a1a] p-8">
38
+ <h2 className="text-2xl font-semibold mb-2">Welcome back</h2>
39
+ <p className="text-[#666] mb-6">Sign in to continue to apteva</p>
40
+
41
+ <form onSubmit={handleSubmit} className="space-y-4">
42
+ <div>
43
+ <label htmlFor="username" className="block text-sm text-[#888] mb-1">
44
+ Username
45
+ </label>
46
+ <input
47
+ id="username"
48
+ type="text"
49
+ value={username}
50
+ onChange={e => setUsername(e.target.value)}
51
+ placeholder="Enter your username"
52
+ autoFocus
53
+ required
54
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
55
+ />
56
+ </div>
57
+
58
+ <div>
59
+ <label htmlFor="password" className="block text-sm text-[#888] mb-1">
60
+ Password
61
+ </label>
62
+ <input
63
+ id="password"
64
+ type="password"
65
+ value={password}
66
+ onChange={e => setPassword(e.target.value)}
67
+ placeholder="Enter your password"
68
+ required
69
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
70
+ />
71
+ </div>
72
+
73
+ {error && (
74
+ <div className="p-3 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm">
75
+ {error}
76
+ </div>
77
+ )}
78
+
79
+ <button
80
+ type="submit"
81
+ disabled={loading || !username || !password}
82
+ className="w-full bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-3 rounded font-medium transition"
83
+ >
84
+ {loading ? "Signing in..." : "Sign In"}
85
+ </button>
86
+ </form>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,2 @@
1
+ export { LoginPage } from "./LoginPage";
2
+ export { CreateAccountStep } from "./CreateAccountStep";
@@ -117,3 +117,51 @@ export function TelemetryIcon({ className = "w-4 h-4" }: IconProps) {
117
117
  </svg>
118
118
  );
119
119
  }
120
+
121
+ export function FilesIcon({ className = "w-4 h-4" }: IconProps) {
122
+ return (
123
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
124
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
125
+ </svg>
126
+ );
127
+ }
128
+
129
+ export function MultiAgentIcon({ className = "w-4 h-4" }: IconProps) {
130
+ return (
131
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
132
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
133
+ </svg>
134
+ );
135
+ }
136
+
137
+ export function MenuIcon({ className = "w-5 h-5" }: IconProps) {
138
+ return (
139
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
140
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
141
+ </svg>
142
+ );
143
+ }
144
+
145
+ export function ChevronDownIcon({ className = "w-4 h-4" }: IconProps) {
146
+ return (
147
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
148
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
149
+ </svg>
150
+ );
151
+ }
152
+
153
+ export function FolderIcon({ className = "w-4 h-4" }: IconProps) {
154
+ return (
155
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
157
+ </svg>
158
+ );
159
+ }
160
+
161
+ export function PlusIcon({ className = "w-4 h-4" }: IconProps) {
162
+ return (
163
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
164
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
165
+ </svg>
166
+ );
167
+ }
@@ -8,7 +8,7 @@ interface ModalProps {
8
8
  export function Modal({ children, onClose }: ModalProps) {
9
9
  return (
10
10
  <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
11
- <div className="bg-[#111] rounded p-6 w-full max-w-xl border border-[#1a1a1a] max-h-[90vh] overflow-y-auto">
11
+ <div className="bg-[#111] rounded p-6 w-full max-w-xl lg:max-w-2xl border border-[#1a1a1a] max-h-[90vh] overflow-y-auto">
12
12
  {children}
13
13
  </div>
14
14
  </div>