@appkit/llamacpp-cli 1.12.0 → 1.13.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 (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,123 +0,0 @@
1
- import { X, Download, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
2
- import { useDownloadJobs, useCancelDownload } from '../hooks/useApi';
3
- import type { DownloadJob } from '../types/api';
4
-
5
- export function DownloadProgress() {
6
- const { data: jobsData } = useDownloadJobs(true);
7
- const cancelMutation = useCancelDownload();
8
-
9
- const jobs = jobsData?.jobs || [];
10
-
11
- // Filter to active jobs only (pending, downloading)
12
- const activeJobs = jobs.filter(
13
- job => job.status === 'pending' || job.status === 'downloading'
14
- );
15
-
16
- // Nothing to show if no active downloads
17
- if (activeJobs.length === 0) return null;
18
-
19
- const handleCancel = (jobId: string) => {
20
- cancelMutation.mutate(jobId);
21
- };
22
-
23
- return (
24
- <div className="fixed bottom-4 right-4 z-40 w-80">
25
- <div className="bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden">
26
- {/* Header */}
27
- <div className="px-4 py-2.5 bg-gray-50 border-b border-gray-200 flex items-center gap-2">
28
- <Download className="w-4 h-4 text-gray-600" />
29
- <span className="text-sm font-medium text-gray-700">
30
- {activeJobs.length} download{activeJobs.length !== 1 ? 's' : ''} in progress
31
- </span>
32
- </div>
33
-
34
- {/* Jobs */}
35
- <div className="max-h-60 overflow-y-auto divide-y divide-gray-100">
36
- {activeJobs.map((job) => (
37
- <JobItem
38
- key={job.id}
39
- job={job}
40
- onCancel={() => handleCancel(job.id)}
41
- />
42
- ))}
43
- </div>
44
- </div>
45
- </div>
46
- );
47
- }
48
-
49
- interface JobItemProps {
50
- job: DownloadJob;
51
- onCancel: () => void;
52
- }
53
-
54
- function JobItem({ job, onCancel }: JobItemProps) {
55
- const formatBytes = (bytes: number) => {
56
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
57
- if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
58
- return `${(bytes / 1e3).toFixed(1)} KB`;
59
- };
60
-
61
- const getStatusIcon = () => {
62
- switch (job.status) {
63
- case 'completed':
64
- return <CheckCircle className="w-4 h-4 text-green-500" />;
65
- case 'failed':
66
- case 'cancelled':
67
- return <AlertCircle className="w-4 h-4 text-red-500" />;
68
- default:
69
- return <Loader2 className="w-4 h-4 text-gray-400 animate-spin" />;
70
- }
71
- };
72
-
73
- return (
74
- <div className="px-4 py-3">
75
- <div className="flex items-center justify-between mb-1">
76
- <div className="flex items-center gap-2 flex-1 min-w-0">
77
- {getStatusIcon()}
78
- <span className="text-sm font-medium text-gray-900 truncate">
79
- {job.filename}
80
- </span>
81
- </div>
82
- {(job.status === 'pending' || job.status === 'downloading') && (
83
- <button
84
- onClick={onCancel}
85
- className="p-1 hover:bg-gray-100 rounded transition-colors cursor-pointer"
86
- title="Cancel download"
87
- >
88
- <X className="w-4 h-4 text-gray-400" />
89
- </button>
90
- )}
91
- </div>
92
-
93
- {job.progress && (
94
- <>
95
- <div className="w-full bg-gray-200 rounded-full h-1.5 mb-1">
96
- <div
97
- className="bg-gray-700 h-1.5 rounded-full transition-all duration-300"
98
- style={{ width: `${job.progress.percentage}%` }}
99
- />
100
- </div>
101
- <div className="flex items-center justify-between text-xs text-gray-500">
102
- <span>
103
- {formatBytes(job.progress.downloaded)} / {formatBytes(job.progress.total)}
104
- </span>
105
- <span>{job.progress.speed}</span>
106
- </div>
107
- </>
108
- )}
109
-
110
- {job.status === 'pending' && !job.progress && (
111
- <p className="text-xs text-gray-500">Starting...</p>
112
- )}
113
-
114
- {job.status === 'failed' && job.error && (
115
- <p className="text-xs text-red-500 truncate">{job.error}</p>
116
- )}
117
-
118
- {job.status === 'cancelled' && (
119
- <p className="text-xs text-gray-500">Cancelled</p>
120
- )}
121
- </div>
122
- );
123
- }
@@ -1,89 +0,0 @@
1
- import { Link, useLocation } from 'react-router-dom';
2
- import { Search, LogOut } from 'lucide-react';
3
- import { api } from '../lib/api';
4
-
5
- interface NavProps {
6
- onLogout?: () => void;
7
- searchQuery?: string;
8
- onSearchChange?: (query: string) => void;
9
- }
10
-
11
- export function Nav({ onLogout, searchQuery = '', onSearchChange }: NavProps) {
12
- const location = useLocation();
13
-
14
- const handleLogout = () => {
15
- api.clearApiKey();
16
- onLogout?.();
17
- };
18
-
19
- return (
20
- <nav className="border-b border-neutral-200 bg-white">
21
- <div className="max-w-7xl mx-auto px-6">
22
- <div className="flex items-center justify-between h-16">
23
- {/* Logo and Nav Links */}
24
- <div className="flex items-center space-x-10">
25
- <Link to="/" className="flex items-center">
26
- <span className="text-xl font-bold text-neutral-900 tracking-tight">LLAMA CPP</span>
27
- </Link>
28
-
29
- <div className="flex items-center space-x-1">
30
- <Link
31
- to="/servers"
32
- className={`px-3 py-2 text-sm font-medium transition-colors rounded-md ${
33
- location.pathname === '/servers' || location.pathname === '/'
34
- ? 'text-neutral-900 bg-neutral-100'
35
- : 'text-neutral-600 hover:text-neutral-900 hover:bg-neutral-50'
36
- }`}
37
- >
38
- Servers
39
- </Link>
40
- <Link
41
- to="/models"
42
- className={`px-3 py-2 text-sm font-medium transition-colors rounded-md ${
43
- location.pathname === '/models'
44
- ? 'text-neutral-900 bg-neutral-100'
45
- : 'text-neutral-600 hover:text-neutral-900 hover:bg-neutral-50'
46
- }`}
47
- >
48
- Models
49
- </Link>
50
- <Link
51
- to="/router"
52
- className={`px-3 py-2 text-sm font-medium transition-colors rounded-md ${
53
- location.pathname === '/router'
54
- ? 'text-neutral-900 bg-neutral-100'
55
- : 'text-neutral-600 hover:text-neutral-900 hover:bg-neutral-50'
56
- }`}
57
- >
58
- Router
59
- </Link>
60
- </div>
61
- </div>
62
-
63
- {/* Search + Logout */}
64
- <div className="flex items-center space-x-3">
65
- {onSearchChange && (
66
- <div className="relative">
67
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400 pointer-events-none" />
68
- <input
69
- type="search"
70
- placeholder="Search models..."
71
- value={searchQuery}
72
- onChange={(e) => onSearchChange(e.target.value)}
73
- className="w-72 pl-9 pr-4 py-2 text-sm text-neutral-900 placeholder:text-neutral-400 border border-neutral-200 rounded-md bg-white hover:border-neutral-300 focus:outline-none focus:border-neutral-400 transition-colors"
74
- />
75
- </div>
76
- )}
77
- <button
78
- onClick={handleLogout}
79
- className="p-2 text-neutral-500 hover:text-neutral-700 hover:bg-neutral-100 rounded-md transition-colors cursor-pointer"
80
- title="Logout"
81
- >
82
- <LogOut className="w-4 h-4" />
83
- </button>
84
- </div>
85
- </div>
86
- </div>
87
- </nav>
88
- );
89
- }
@@ -1,240 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { X, Loader2, Save } from 'lucide-react';
3
- import { useUpdateRouter } from '../hooks/useApi';
4
- import type { RouterInfo } from '../types/api';
5
-
6
- interface RouterConfigModalProps {
7
- router: RouterInfo | null;
8
- isOpen: boolean;
9
- onClose: () => void;
10
- }
11
-
12
- interface FormData {
13
- port: number;
14
- host: string;
15
- verbose: boolean;
16
- requestTimeout: number;
17
- healthCheckInterval: number;
18
- }
19
-
20
- export function RouterConfigModal({ router, isOpen, onClose }: RouterConfigModalProps) {
21
- const updateRouter = useUpdateRouter();
22
-
23
- const [formData, setFormData] = useState<FormData>({
24
- port: 9100,
25
- host: '127.0.0.1',
26
- verbose: false,
27
- requestTimeout: 120000,
28
- healthCheckInterval: 5000,
29
- });
30
-
31
- const [restartAfterSave, setRestartAfterSave] = useState(true);
32
- const [error, setError] = useState<string | null>(null);
33
-
34
- // Initialize form when router changes
35
- useEffect(() => {
36
- if (router?.config) {
37
- setFormData({
38
- port: router.config.port,
39
- host: router.config.host,
40
- verbose: router.config.verbose,
41
- requestTimeout: router.config.requestTimeout,
42
- healthCheckInterval: router.config.healthCheckInterval,
43
- });
44
- setError(null);
45
- }
46
- }, [router]);
47
-
48
- const handleSubmit = async (e: React.FormEvent) => {
49
- e.preventDefault();
50
- if (!router) return;
51
-
52
- setError(null);
53
-
54
- try {
55
- const result = await updateRouter.mutateAsync({
56
- port: formData.port,
57
- host: formData.host,
58
- verbose: formData.verbose,
59
- requestTimeout: formData.requestTimeout,
60
- healthCheckInterval: formData.healthCheckInterval,
61
- });
62
-
63
- // If router is running and needs restart, prompt user
64
- if (result.needsRestart && router.isRunning && !restartAfterSave) {
65
- setError('Changes saved. Restart the router to apply them.');
66
- return;
67
- }
68
-
69
- onClose();
70
- } catch (err) {
71
- setError((err as Error).message);
72
- }
73
- };
74
-
75
- if (!isOpen || !router) return null;
76
-
77
- return (
78
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
79
- <div className="bg-white rounded-xl shadow-xl w-full max-w-md mx-4 max-h-[90vh] flex flex-col">
80
- {/* Header */}
81
- <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
82
- <div>
83
- <h2 className="text-lg font-semibold text-gray-900">Configure Router</h2>
84
- <p className="text-sm text-gray-500">Unified model routing service</p>
85
- </div>
86
- <button
87
- onClick={onClose}
88
- className="p-1 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
89
- >
90
- <X className="w-5 h-5" />
91
- </button>
92
- </div>
93
-
94
- {/* Form */}
95
- <form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-4 space-y-4">
96
- {/* Port */}
97
- <div>
98
- <label className="block text-sm font-medium text-gray-700 mb-1">
99
- Port
100
- </label>
101
- <input
102
- type="number"
103
- value={formData.port}
104
- onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) || 9100 })}
105
- min={1024}
106
- max={65535}
107
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
108
- />
109
- <p className="text-xs text-gray-500 mt-1">Router service port (requires restart)</p>
110
- </div>
111
-
112
- {/* Host */}
113
- <div>
114
- <label className="block text-sm font-medium text-gray-700 mb-1">
115
- Host
116
- </label>
117
- <select
118
- value={formData.host}
119
- onChange={(e) => setFormData({ ...formData, host: e.target.value })}
120
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent bg-white"
121
- >
122
- <option value="127.0.0.1">127.0.0.1 (localhost only)</option>
123
- <option value="0.0.0.0">0.0.0.0 (all interfaces)</option>
124
- </select>
125
- <p className="text-xs text-gray-500 mt-1">Network interface (requires restart)</p>
126
- </div>
127
-
128
- {/* Request Timeout */}
129
- <div>
130
- <label className="block text-sm font-medium text-gray-700 mb-1">
131
- Request Timeout
132
- </label>
133
- <input
134
- type="number"
135
- value={formData.requestTimeout / 1000}
136
- onChange={(e) => setFormData({ ...formData, requestTimeout: (parseInt(e.target.value) || 120) * 1000 })}
137
- min={10}
138
- max={600}
139
- step={10}
140
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
141
- />
142
- <p className="text-xs text-gray-500 mt-1">
143
- {formData.requestTimeout / 1000}s - Maximum time to wait for backend responses
144
- </p>
145
- </div>
146
-
147
- {/* Health Check Interval */}
148
- <div>
149
- <label className="block text-sm font-medium text-gray-700 mb-1">
150
- Health Check Interval
151
- </label>
152
- <input
153
- type="number"
154
- value={formData.healthCheckInterval / 1000}
155
- onChange={(e) => setFormData({ ...formData, healthCheckInterval: (parseInt(e.target.value) || 5) * 1000 })}
156
- min={1}
157
- max={60}
158
- className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
159
- />
160
- <p className="text-xs text-gray-500 mt-1">
161
- {formData.healthCheckInterval / 1000}s - How often to check backend server health
162
- </p>
163
- </div>
164
-
165
- {/* Verbose */}
166
- <div className="flex items-center justify-between">
167
- <div>
168
- <label className="text-sm font-medium text-gray-700">Verbose Logging</label>
169
- <p className="text-xs text-gray-500">Log detailed request information</p>
170
- </div>
171
- <button
172
- type="button"
173
- onClick={() => setFormData({ ...formData, verbose: !formData.verbose })}
174
- className={`relative w-11 h-6 rounded-full transition-colors cursor-pointer ${
175
- formData.verbose ? 'bg-gray-900' : 'bg-gray-200'
176
- }`}
177
- >
178
- <span
179
- className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${
180
- formData.verbose ? 'translate-x-5' : ''
181
- }`}
182
- />
183
- </button>
184
- </div>
185
-
186
- {/* Restart option (only show if router is running) */}
187
- {router.isRunning && (
188
- <div className="flex items-center gap-2 pt-2 border-t border-gray-100">
189
- <input
190
- type="checkbox"
191
- id="restartAfterSave"
192
- checked={restartAfterSave}
193
- onChange={(e) => setRestartAfterSave(e.target.checked)}
194
- className="w-4 h-4 text-gray-900 border-gray-300 rounded focus:ring-gray-200"
195
- />
196
- <label htmlFor="restartAfterSave" className="text-sm text-gray-700">
197
- Restart router to apply changes
198
- </label>
199
- </div>
200
- )}
201
-
202
- {/* Error */}
203
- {error && (
204
- <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
205
- <p className="text-sm text-red-700">{error}</p>
206
- </div>
207
- )}
208
- </form>
209
-
210
- {/* Footer */}
211
- <div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-gray-200">
212
- <button
213
- type="button"
214
- onClick={onClose}
215
- className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
216
- >
217
- Cancel
218
- </button>
219
- <button
220
- onClick={handleSubmit}
221
- disabled={updateRouter.isPending}
222
- className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-wait cursor-pointer"
223
- >
224
- {updateRouter.isPending ? (
225
- <>
226
- <Loader2 className="w-4 h-4 animate-spin" />
227
- Saving...
228
- </>
229
- ) : (
230
- <>
231
- <Save className="w-4 h-4" />
232
- Save Changes
233
- </>
234
- )}
235
- </button>
236
- </div>
237
- </div>
238
- </div>
239
- );
240
- }