@zenith-open/zenithcms-plugin-ai-architect-ui 1.0.0-beta.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aman T Shekar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ export interface PageHeaderProps {
3
+ title: React.ReactNode;
4
+ description?: React.ReactNode;
5
+ icon?: React.ReactNode;
6
+ actions?: React.ReactNode;
7
+ className?: string;
8
+ backLink?: {
9
+ to: string;
10
+ label: string;
11
+ };
12
+ breadcrumbs?: any;
13
+ }
14
+ export declare function PageHeader({ title, description, icon, actions, className, backLink }: PageHeaderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '../../lib/utils';
3
+ import { useTheme } from '../../context/ThemeContext';
4
+ export function PageHeader({ title, description, icon, actions, className, backLink }) {
5
+ const { theme } = useTheme();
6
+ return (_jsxs("div", { className: cn("px-6 py-4 border-b flex items-center justify-between transition-colors", theme === 'dark' ? 'bg-[var(--z-bg-modal)] backdrop-blur-md border-z-border' : 'bg-z-panel/80 backdrop-blur-md border-z-border', className), children: [_jsxs("div", { className: "flex items-center gap-4", children: [backLink && (_jsx("a", { href: backLink.to, className: "flex items-center justify-center p-2 rounded-none border border-transparent hover:border-z-border hover:bg-z-hover transition-all text-z-secondary", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-arrow-left", children: [_jsx("path", { d: "m12 19-7-7 7-7" }), _jsx("path", { d: "M19 12H5" })] }) })), icon && (_jsx("div", { className: cn("p-2.5 rounded-none-none border", theme === 'dark' ? 'bg-z-panel border-z-border text-z-active-text' : 'bg-z-input border-z-border text-z-accent'), children: icon })), _jsxs("div", { children: [_jsx("h1", { className: cn("text-xl font-semibold leading-none", 'text-z-primary'), children: title }), description && (_jsx("p", { className: cn("text-xs mt-1", theme === 'dark' ? 'text-z-muted' : 'text-z-secondary'), children: description }))] })] }), actions && (_jsx("div", { className: "flex items-center gap-3", children: actions }))] }));
7
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ type Theme = 'light' | 'dark';
3
+ interface ThemeContextType {
4
+ theme: Theme;
5
+ toggleTheme: () => void;
6
+ setTheme: (theme: Theme) => void;
7
+ }
8
+ export declare const ThemeProvider: React.FC<{
9
+ children: React.ReactNode;
10
+ }>;
11
+ export declare const useTheme: () => ThemeContextType;
12
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState } from 'react';
3
+ const ThemeContext = createContext(undefined);
4
+ export const ThemeProvider = ({ children }) => {
5
+ const [theme, setThemeState] = useState(localStorage.getItem('zenith_theme') || 'dark');
6
+ useEffect(() => {
7
+ const root = window.document.documentElement;
8
+ root.classList.remove('light', 'dark');
9
+ root.classList.add(theme);
10
+ localStorage.setItem('zenith_theme', theme);
11
+ }, [theme]);
12
+ const setTheme = (newTheme) => {
13
+ setThemeState(newTheme);
14
+ };
15
+ const toggleTheme = () => {
16
+ setThemeState(prev => prev === 'dark' ? 'light' : 'dark');
17
+ };
18
+ return (_jsx(ThemeContext.Provider, { value: { theme, toggleTheme, setTheme }, children: children }));
19
+ };
20
+ export const useTheme = () => {
21
+ const context = useContext(ThemeContext);
22
+ if (!context)
23
+ throw new Error('useTheme must be used within ThemeProvider');
24
+ return context;
25
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Unified, typed error for all API-layer failures.
3
+ *
4
+ * Every `api` call throws `ApiError` on failure, so callers can catch
5
+ * with predictable properties instead of casting `unknown` / `any`.
6
+ *
7
+ * ```ts
8
+ * import api from './api'
9
+ * import { ApiError } from './ApiError'
10
+ *
11
+ * try {
12
+ * await api.post('/collections', data)
13
+ * } catch (err) {
14
+ * if (err instanceof ApiError) {
15
+ * console.error((err as { status?: number }).status, (err as { code?: string }).code, (err instanceof Error ? err.message : String(err)))
16
+ * }
17
+ * }
18
+ * ```
19
+ */
20
+ export declare class ApiError extends Error {
21
+ /** HTTP status code, or 0 for network/tenant errors */
22
+ status: number;
23
+ /** Machine-readable short code: 'ERR_NETWORK', 'ERR_NO_TENANT', 'ERR_CSRF', etc. */
24
+ code: string;
25
+ /** The raw response payload (if any) */
26
+ response?: {
27
+ data: any;
28
+ status: number;
29
+ headers?: Headers;
30
+ };
31
+ constructor(opts: {
32
+ message: string;
33
+ status?: number;
34
+ code?: string;
35
+ response?: {
36
+ data: any;
37
+ status: number;
38
+ headers?: Headers;
39
+ };
40
+ });
41
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Unified, typed error for all API-layer failures.
3
+ *
4
+ * Every `api` call throws `ApiError` on failure, so callers can catch
5
+ * with predictable properties instead of casting `unknown` / `any`.
6
+ *
7
+ * ```ts
8
+ * import api from './api'
9
+ * import { ApiError } from './ApiError'
10
+ *
11
+ * try {
12
+ * await api.post('/collections', data)
13
+ * } catch (err) {
14
+ * if (err instanceof ApiError) {
15
+ * console.error((err as { status?: number }).status, (err as { code?: string }).code, (err instanceof Error ? err.message : String(err)))
16
+ * }
17
+ * }
18
+ * ```
19
+ */
20
+ export class ApiError extends Error {
21
+ /** HTTP status code, or 0 for network/tenant errors */
22
+ status;
23
+ /** Machine-readable short code: 'ERR_NETWORK', 'ERR_NO_TENANT', 'ERR_CSRF', etc. */
24
+ code;
25
+ /** The raw response payload (if any) */
26
+ response;
27
+ constructor(opts) {
28
+ super(opts.message);
29
+ this.name = 'ApiError';
30
+ this.status = opts.status ?? 0;
31
+ this.code = opts.code ?? 'ERR_UNKNOWN';
32
+ this.response = opts.response;
33
+ }
34
+ }
@@ -0,0 +1,31 @@
1
+ interface ApiResponse<T = any> {
2
+ data: T;
3
+ status: number;
4
+ headers: Headers;
5
+ }
6
+ declare const apiInstance: {
7
+ defaults: {
8
+ headers: Record<string, string>;
9
+ };
10
+ get<T = any>(path: string, config?: {
11
+ params?: any;
12
+ headers?: Record<string, string>;
13
+ }): Promise<ApiResponse<T>>;
14
+ post<T = any>(path: string, body?: any, config?: {
15
+ headers?: Record<string, string>;
16
+ params?: any;
17
+ }): Promise<ApiResponse<T>>;
18
+ patch<T = any>(path: string, body?: any, config?: {
19
+ headers?: Record<string, string>;
20
+ params?: any;
21
+ }): Promise<ApiResponse<T>>;
22
+ put<T = any>(path: string, body?: any, config?: {
23
+ headers?: Record<string, string>;
24
+ params?: any;
25
+ }): Promise<ApiResponse<T>>;
26
+ delete<T = any>(path: string, config?: {
27
+ headers?: Record<string, string>;
28
+ params?: any;
29
+ }): Promise<ApiResponse<T>>;
30
+ };
31
+ export default apiInstance;
@@ -0,0 +1,240 @@
1
+ import { useTenantStore } from './tenantStore';
2
+ import { ApiError } from './ApiError';
3
+ let isRefreshing = false;
4
+ let failedQueue = [];
5
+ const processQueue = (error, _token = null) => {
6
+ failedQueue.forEach((prom) => {
7
+ if (error) {
8
+ prom.reject(error);
9
+ }
10
+ else {
11
+ prom.resolve(_token);
12
+ }
13
+ });
14
+ failedQueue = [];
15
+ };
16
+ // Wrapped version ensures rejection handler throws don't break the redirect
17
+ const safeProcessQueue = (error, token) => {
18
+ try {
19
+ processQueue(error, token);
20
+ }
21
+ catch { /* ignore — queue handlers should not throw */ }
22
+ };
23
+ const getCookie = (name) => {
24
+ if (typeof document === 'undefined')
25
+ return null;
26
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
27
+ return match ? decodeURIComponent(match[2]) : null;
28
+ };
29
+ // Security: Never hardcode localhost as a fallback in production.
30
+ // If VITE_API_URL is not set, fall back to '/api/v1' (same-host relative path).
31
+ // If the admin and API are on different hosts, VITE_API_URL must be explicitly configured.
32
+ let BASE_URL = import.meta.env.VITE_API_URL || '/api/v1';
33
+ if (import.meta.env.PROD && BASE_URL.startsWith('http://') && !BASE_URL.includes('localhost') && !BASE_URL.includes('127.0.0.1')) {
34
+ console.warn('Insecure HTTP API URL used in production. Upgrading to HTTPS.');
35
+ BASE_URL = BASE_URL.replace('http://', 'https://');
36
+ }
37
+ function buildUrl(path, config) {
38
+ if (path.startsWith('http'))
39
+ return path;
40
+ let url = `${BASE_URL}${path}`;
41
+ if (config?.params) {
42
+ const searchParams = new URLSearchParams();
43
+ for (const [key, value] of Object.entries(config.params)) {
44
+ if (value !== undefined && value !== null) {
45
+ searchParams.append(key, String(value));
46
+ }
47
+ }
48
+ const qs = searchParams.toString();
49
+ if (qs)
50
+ url += (path.includes('?') ? '&' : '?') + qs;
51
+ }
52
+ return url;
53
+ }
54
+ function isFormData(body) {
55
+ return typeof FormData !== 'undefined' && body instanceof FormData;
56
+ }
57
+ async function fetchWithAuth(method, path, body, config) {
58
+ const url = buildUrl(path, config);
59
+ const headers = {
60
+ 'Content-Type': 'application/json',
61
+ ...config?.headers,
62
+ };
63
+ const storeToken = useTenantStore.getState().token;
64
+ if (storeToken) {
65
+ headers['Authorization'] = `Bearer ${storeToken}`;
66
+ }
67
+ // Apply default headers set dynamically via api.defaults.headers
68
+ if (apiInstance.defaults.headers) {
69
+ for (const [key, value] of Object.entries(apiInstance.defaults.headers)) {
70
+ if (value !== undefined && !headers[key]) {
71
+ headers[key] = value;
72
+ }
73
+ }
74
+ }
75
+ // Ensure active site ID is dynamically set on every request to prevent tenant leaking
76
+ const currentSiteId = useTenantStore.getState().activeSiteId;
77
+ if (currentSiteId && !headers['x-zenith-site-id']) {
78
+ headers['x-zenith-site-id'] = currentSiteId;
79
+ }
80
+ // Hard tenant guard — abort any tenant-scoped request missing x-zenith-site-id.
81
+ // Exempt: auth, site listing, uploads, health, and protocol paths that run before
82
+ // a site is selected (globals editor, document locks, collab presence, media proxy).
83
+ const isTenantExempt = path.startsWith('/auth') ||
84
+ path === '/sites' ||
85
+ path.startsWith('/sites?') ||
86
+ path.startsWith('/sites/') ||
87
+ path.startsWith('/uploads') ||
88
+ path.startsWith('/health') ||
89
+ path.startsWith('/system') ||
90
+ path.startsWith('/globals') ||
91
+ path.startsWith('/locks') ||
92
+ path.startsWith('/presence') ||
93
+ path.startsWith('/media') ||
94
+ path.startsWith('/versions') ||
95
+ path.startsWith('/releases') ||
96
+ path.startsWith('/workspaces');
97
+ if (!currentSiteId && !headers['x-zenith-site-id'] && !isTenantExempt) {
98
+ throw new ApiError({
99
+ message: 'Missing tenant context: x-zenith-site-id is required for this request. ' +
100
+ 'Ensure activeSiteId is set in localStorage before making API calls.',
101
+ code: 'ERR_NO_TENANT',
102
+ });
103
+ }
104
+ // Double-Submit Cookie CSRF for mutating requests
105
+ if (['post', 'put', 'delete', 'patch'].includes(method.toLowerCase())) {
106
+ const csrfToken = getCookie('XSRF-TOKEN');
107
+ if (csrfToken) {
108
+ headers['x-csrf-token'] = csrfToken;
109
+ }
110
+ }
111
+ // Don't set Content-Type for FormData (browser sets it with boundary)
112
+ if (isFormData(body)) {
113
+ delete headers['Content-Type'];
114
+ }
115
+ const fetchOptions = {
116
+ method,
117
+ headers,
118
+ credentials: 'include',
119
+ };
120
+ if (body !== undefined) {
121
+ fetchOptions.body = isFormData(body) ? body : JSON.stringify(body);
122
+ }
123
+ try {
124
+ return await fetch(url, fetchOptions).then(async (response) => {
125
+ let data;
126
+ try {
127
+ data = await response.json();
128
+ }
129
+ catch {
130
+ data = null;
131
+ }
132
+ return { data, status: response.status, headers: response.headers };
133
+ });
134
+ }
135
+ catch {
136
+ // Network error — throw ApiError so callers catch a consistent shape
137
+ throw new ApiError({ message: 'Network Error', code: 'ERR_NETWORK' });
138
+ }
139
+ }
140
+ async function request(method, path, body, config) {
141
+ const result = await fetchWithAuth(method, path, body, config);
142
+ // Handle 401 with token refresh (skip for login requests, as 401 means invalid credentials)
143
+ if (result.status === 401 && !path.includes('/login')) {
144
+ if (isRefreshing) {
145
+ // Queue this request until refresh completes
146
+ return new Promise((resolve, reject) => {
147
+ failedQueue.push({ resolve, reject });
148
+ }).then(async () => {
149
+ return fetchWithAuth(method, path, body, config);
150
+ });
151
+ }
152
+ isRefreshing = true;
153
+ try {
154
+ const newToken = await refreshToken();
155
+ safeProcessQueue(null, newToken);
156
+ return fetchWithAuth(method, path, body, config);
157
+ }
158
+ catch (refreshError) {
159
+ safeProcessQueue(refreshError, null);
160
+ if (refreshError?.status === 401 && !window.location.pathname.includes('/login')) {
161
+ localStorage.clear();
162
+ window.location.href = '/login';
163
+ }
164
+ throw refreshError;
165
+ }
166
+ finally {
167
+ isRefreshing = false;
168
+ }
169
+ }
170
+ // Throw for non-2xx status codes (ApiError with response payload)
171
+ if (result.status >= 400) {
172
+ const errorMsg = result.data?.error?.message || result.data?.message || '';
173
+ if (result.status === 403 && (errorMsg.includes('Access denied to this site') || errorMsg.includes('site_access') || errorMsg.includes('Forbidden'))) {
174
+ useTenantStore.getState().setActiveSiteId('');
175
+ localStorage.removeItem('activeSiteId');
176
+ if (!window.location.pathname.includes('/sites')) {
177
+ window.location.href = '/sites';
178
+ }
179
+ }
180
+ throw new ApiError({
181
+ message: result.data?.message || `Request failed with status ${result.status}`,
182
+ status: result.status,
183
+ code: 'ERR_HTTP',
184
+ response: { data: result.data, status: result.status, headers: result.headers },
185
+ });
186
+ }
187
+ return result;
188
+ }
189
+ async function refreshToken() {
190
+ const res = await fetch(`${BASE_URL}/auth/refresh`, {
191
+ method: 'POST',
192
+ credentials: 'include',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ });
195
+ if (!res.ok)
196
+ throw new ApiError({ message: 'Token refresh failed', status: res.status, code: 'ERR_REFRESH' });
197
+ const body = await res.json();
198
+ const newToken = body?.token || body?.accessToken;
199
+ if (newToken) {
200
+ useTenantStore.getState().setToken(newToken);
201
+ }
202
+ return newToken || '';
203
+ }
204
+ const getInitialSiteId = () => {
205
+ if (typeof window !== 'undefined' && window.localStorage) {
206
+ // First try from the store (which persists), fall back to raw localStorage for backward compatibility
207
+ const storeSiteId = useTenantStore.getState().activeSiteId;
208
+ if (storeSiteId)
209
+ return storeSiteId;
210
+ const legacySiteId = window.localStorage.getItem('activeSiteId');
211
+ if (legacySiteId) {
212
+ useTenantStore.getState().setActiveSiteId(legacySiteId);
213
+ return legacySiteId;
214
+ }
215
+ }
216
+ return '';
217
+ };
218
+ const apiInstance = {
219
+ defaults: {
220
+ headers: {
221
+ ...(getInitialSiteId() ? { 'x-zenith-site-id': getInitialSiteId() } : {})
222
+ },
223
+ },
224
+ async get(path, config) {
225
+ return request('GET', path, undefined, config);
226
+ },
227
+ async post(path, body, config) {
228
+ return request('POST', path, body, config);
229
+ },
230
+ async patch(path, body, config) {
231
+ return request('PATCH', path, body, config);
232
+ },
233
+ async put(path, body, config) {
234
+ return request('PUT', path, body, config);
235
+ },
236
+ async delete(path, config) {
237
+ return request('DELETE', path, undefined, config);
238
+ },
239
+ };
240
+ export default apiInstance;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Central store for authentication and multi‑tenant context.
3
+ * It persists to localStorage so the values survive page reloads.
4
+ */
5
+ interface TenantState {
6
+ token: string | null;
7
+ activeSiteId: string | null;
8
+ activeSiteName: string | null;
9
+ setToken: (token: string | null) => void;
10
+ setActiveSiteId: (siteId: string | null, siteName?: string | null) => void;
11
+ }
12
+ export declare const useTenantStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<TenantState>, "setState" | "persist"> & {
13
+ setState(partial: TenantState | Partial<TenantState> | ((state: TenantState) => TenantState | Partial<TenantState>), replace?: false | undefined): unknown;
14
+ setState(state: TenantState | ((state: TenantState) => TenantState), replace: true): unknown;
15
+ persist: {
16
+ setOptions: (options: Partial<import("zustand/middleware").PersistOptions<TenantState, {
17
+ activeSiteId: string | null;
18
+ activeSiteName: string | null;
19
+ }, unknown>>) => void;
20
+ clearStorage: () => void;
21
+ rehydrate: () => Promise<void> | void;
22
+ hasHydrated: () => boolean;
23
+ onHydrate: (fn: (state: TenantState) => void) => () => void;
24
+ onFinishHydration: (fn: (state: TenantState) => void) => () => void;
25
+ getOptions: () => Partial<import("zustand/middleware").PersistOptions<TenantState, {
26
+ activeSiteId: string | null;
27
+ activeSiteName: string | null;
28
+ }, unknown>>;
29
+ };
30
+ }>;
31
+ export {};
@@ -0,0 +1,46 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+ function getLegacyToken() {
4
+ try {
5
+ return localStorage.getItem('token');
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ function getLegacySiteId() {
12
+ try {
13
+ return localStorage.getItem('activeSiteId');
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function getLegacySiteName() {
20
+ try {
21
+ return localStorage.getItem('activeSiteName');
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export const useTenantStore = create()(persist((set) => ({
28
+ // First try the persisted store; fall back to legacy localStorage keys
29
+ token: getLegacyToken(),
30
+ activeSiteId: getLegacySiteId(),
31
+ activeSiteName: getLegacySiteName(),
32
+ setToken: (token) => set({ token }),
33
+ setActiveSiteId: (siteId, siteName) => set((state) => ({
34
+ activeSiteId: siteId,
35
+ activeSiteName: siteName ?? state.activeSiteName,
36
+ })),
37
+ }), {
38
+ name: 'zenith-tenant-store',
39
+ // SECURITY: Never persist the auth token to localStorage — it must live in
40
+ // memory only. The server uses HttpOnly cookies for actual authentication.
41
+ // Storing the token in localStorage makes it extractable via XSS attacks.
42
+ partialize: (state) => ({
43
+ activeSiteId: state.activeSiteId,
44
+ activeSiteName: state.activeSiteName,
45
+ }),
46
+ }));
@@ -0,0 +1,15 @@
1
+ import { type ClassValue } from 'clsx';
2
+ /**
3
+ * Utility for merging Tailwind classes with clsx
4
+ */
5
+ export declare function cn(...inputs: ClassValue[]): string;
6
+ /**
7
+ * Extracts purely text strings from a nested JSON structure (e.g. Zenith blocks)
8
+ * Ideal for passing document content to AI APIs.
9
+ */
10
+ export declare function extractTextFromBlocks(content: any): string;
11
+ /**
12
+ * Generate a stable unique ID. Uses crypto.randomUUID() with a safe fallback
13
+ * for non-HTTPS contexts where the API throws a DOMException.
14
+ */
15
+ export declare function uid(): string;
@@ -0,0 +1,40 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ /**
4
+ * Utility for merging Tailwind classes with clsx
5
+ */
6
+ export function cn(...inputs) {
7
+ return twMerge(clsx(inputs));
8
+ }
9
+ /**
10
+ * Extracts purely text strings from a nested JSON structure (e.g. Zenith blocks)
11
+ * Ideal for passing document content to AI APIs.
12
+ */
13
+ export function extractTextFromBlocks(content) {
14
+ if (!content)
15
+ return '';
16
+ if (typeof content === 'string')
17
+ return content;
18
+ if (Array.isArray(content)) {
19
+ return content.map(extractTextFromBlocks).filter(Boolean).join(' ');
20
+ }
21
+ if (typeof content === 'object') {
22
+ return Object.values(content).map(extractTextFromBlocks).filter(Boolean).join(' ');
23
+ }
24
+ return String(content);
25
+ }
26
+ /**
27
+ * Generate a stable unique ID. Uses crypto.randomUUID() with a safe fallback
28
+ * for non-HTTPS contexts where the API throws a DOMException.
29
+ */
30
+ export function uid() {
31
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
32
+ try {
33
+ return crypto.randomUUID();
34
+ }
35
+ catch {
36
+ // Fallback for non-secure contexts (http, localhost without HTTPS)
37
+ }
38
+ }
39
+ return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
40
+ }
@@ -0,0 +1,2 @@
1
+ declare const AIWriterPage: () => import("react").JSX.Element;
2
+ export default AIWriterPage;
@@ -0,0 +1,192 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import { Sparkles, Send, Loader2, Copy, Zap, Terminal, Cpu, PenTool, ShieldCheck, Search, Image as ImageIcon, CheckCircle2, AlertCircle, ChevronRight, RotateCcw, Save, Download, Code2, Wand2, Hash } from 'lucide-react';
4
+ import api from '../../admin/src/lib/api';
5
+ import { motion, AnimatePresence } from 'framer-motion';
6
+ import { cn } from '../../admin/src/lib/utils';
7
+ import toast from 'react-hot-toast';
8
+ import { useTheme } from '../../admin/src/context/ThemeContext';
9
+ import { PageHeader } from '../../admin/src/components/ui/PageHeader';
10
+ // ── Tool Definitions ───────────────────────────────────────────────────────────
11
+ const TOOLS = [
12
+ { id: 'seo', name: 'SEO Analysis', icon: Search, endpoint: '/content-tools/seo-analysis', desc: 'Score title, meta, content', color: 'text-amber-400' },
13
+ { id: 'quality', name: 'Quality Audit', icon: ShieldCheck, endpoint: '/content-tools/quality', desc: 'Readability + word structure', color: 'text-z-active-text' },
14
+ { id: 'improve', name: 'Refine Text', icon: Wand2, endpoint: '/content-tools/ai/improve', desc: 'AI-powered rewrite', color: 'text-purple-400' },
15
+ { id: 'meta', name: 'Meta Generator', icon: Hash, endpoint: '/content-tools/ai/meta-description', desc: 'Auto SEO meta description', color: 'text-z-active-text' },
16
+ { id: 'alt', name: 'Alt Text', icon: ImageIcon, endpoint: '/content-tools/ai/alt-text', desc: 'Generate image alt text', color: 'text-pink-400' },
17
+ ];
18
+ const MODES = [
19
+ { id: 'writer', label: 'Writer', icon: PenTool, desc: 'Generate content from a prompt' },
20
+ { id: 'architect', label: 'Architect', icon: Cpu, desc: 'Design a collection schema with AI' },
21
+ { id: 'tools', label: 'Tools', icon: Zap, desc: 'SEO, quality, meta & more' },
22
+ ];
23
+ // ── Result Renderers ──────────────────────────────────────────────────────────
24
+ function SeoResultView({ data }) {
25
+ return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "flex items-end gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-secondary mb-1", children: "SEO Score" }), _jsx("span", { className: cn('text-5xl font-black tabular-nums', data.score >= 70 ? 'text-z-active-text' : data.score >= 45 ? 'text-amber-400' : 'text-red-400'), children: data.score }), _jsx("span", { className: "text-lg text-z-secondary font-black", children: "/100" })] }), _jsx("div", { className: "flex-1 h-2 bg-z-hover rounded-full overflow-hidden mb-3", children: _jsx("div", { className: cn('h-full transition-all duration-700', data.score >= 70 ? 'bg-z-accent' : data.score >= 45 ? 'bg-amber-500' : 'bg-red-500'), style: { width: `${data.score}%` } }) })] }), data.passed?.length > 0 && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-active-text/70", children: "Passing" }), data.passed.map((p, i) => (_jsxs("div", { className: "flex items-center gap-2.5 text-[10px] text-z-muted", children: [_jsx(CheckCircle2, { size: 11, className: "text-z-active-text flex-shrink-0" }), p] }, i)))] })), data.issues?.length > 0 && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-red-500/70", children: "Issues" }), data.issues.map((p, i) => (_jsxs("div", { className: "flex items-center gap-2.5 text-[10px] text-z-muted", children: [_jsx(AlertCircle, { size: 11, className: "text-red-500 flex-shrink-0" }), p] }, i)))] })), data.suggestions?.length > 0 && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-amber-500/70", children: "Suggestions" }), data.suggestions.map((p, i) => (_jsxs("div", { className: "flex items-center gap-2.5 text-[10px] text-z-muted", children: [_jsx(ChevronRight, { size: 11, className: "text-amber-500 flex-shrink-0" }), p] }, i)))] }))] }));
26
+ }
27
+ function QualityResultView({ data }) {
28
+ const gradeColor = { A: 'text-z-active-text', B: 'text-z-active-text', C: 'text-amber-400', D: 'text-orange-400', F: 'text-red-400' };
29
+ return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "flex items-start gap-6", children: [_jsxs("div", { children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-secondary mb-1", children: "Grade" }), _jsx("span", { className: cn('text-6xl font-black', gradeColor[data.grade] || 'text-white'), children: data.grade })] }), _jsx("div", { className: "grid grid-cols-3 gap-3 flex-1 pt-1", children: [
30
+ { label: 'Words', val: data.wordCount },
31
+ { label: 'Sentences', val: data.sentenceCount },
32
+ { label: 'Avg Words/Sent', val: data.avgWordsPerSentence },
33
+ ].map(m => (_jsxs("div", { className: "bg-z-hover border border-z-border p-3", children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-widest text-z-secondary mb-1", children: m.label }), _jsx("p", { className: "text-lg font-black text-white tabular-nums", children: m.val })] }, m.label))) })] }), data.issues?.length > 0 && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-red-500/70", children: "Issues" }), data.issues.map((p, i) => _jsxs("div", { className: "flex items-center gap-2.5 text-[10px] text-z-muted", children: [_jsx(AlertCircle, { size: 11, className: "text-red-500 flex-shrink-0" }), p] }, i))] })), data.suggestions?.length > 0 && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-active-text/70", children: "Suggestions" }), data.suggestions.map((p, i) => _jsxs("div", { className: "flex items-center gap-2.5 text-[10px] text-z-muted", children: [_jsx(CheckCircle2, { size: 11, className: "text-z-active-text flex-shrink-0" }), p] }, i))] }))] }));
34
+ }
35
+ function SchemaResultView({ data }) {
36
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-black text-white tracking-tight", children: data.name }), _jsxs("p", { className: "text-[9px] text-z-secondary font-mono mt-0.5", children: ["/", data.slug] })] }), _jsxs("div", { className: "flex gap-2", children: [data.drafts && _jsx("span", { className: "px-2 py-0.5 text-[7px] font-black uppercase tracking-widest bg-z-accent/10 border border-z-accent/20 text-z-active-text", children: "Drafts" }), data.timestamps && _jsx("span", { className: "px-2 py-0.5 text-[7px] font-black uppercase tracking-widest bg-gray-500/10 border border-gray-500/20 text-z-muted", children: "Timestamps" })] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-secondary", children: [data.fields?.length || 0, " Fields"] }), data.fields?.map((f, i) => (_jsxs("div", { className: "flex items-center justify-between px-3 py-2 bg-z-hover border border-z-border hover:border-z-border-strong transition-colors group", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-[9px] font-mono text-z-active-text", children: f.name }), f.required && _jsx("span", { className: "text-[6px] font-black text-red-500 uppercase tracking-wider", children: "required" })] }), _jsx("span", { className: "text-[8px] text-gray-600 uppercase tracking-widest font-black", children: f.type })] }, i)))] })] }));
37
+ }
38
+ // ── Main Component ─────────────────────────────────────────────────────────────
39
+ const AIWriterPage = () => {
40
+ const { theme } = useTheme();
41
+ const dark = theme === 'dark';
42
+ const [mode, setMode] = useState('writer');
43
+ const [prompt, setPrompt] = useState('');
44
+ const [loading, setLoading] = useState(false);
45
+ const [result, setResult] = useState(null);
46
+ const [activeTool, setActiveTool] = useState('seo');
47
+ const [history, setHistory] = useState([]);
48
+ const [schemaForSave, setSchemaForSave] = useState(null);
49
+ const [savingSchema, setSavingSchema] = useState(false);
50
+ const textareaRef = useRef(null);
51
+ // Auto-resize textarea
52
+ useEffect(() => {
53
+ if (textareaRef.current) {
54
+ textareaRef.current.style.height = 'auto';
55
+ textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 300)}px`;
56
+ }
57
+ }, [prompt]);
58
+ const handleExecute = async () => {
59
+ if (!prompt.trim() && activeTool !== 'alt')
60
+ return;
61
+ setLoading(true);
62
+ setResult(null);
63
+ setSchemaForSave(null);
64
+ try {
65
+ let res;
66
+ let resultData;
67
+ if (mode === 'architect') {
68
+ res = await api.post('/system/ai-architect', { prompt });
69
+ resultData = res.data.data.schema;
70
+ setSchemaForSave(resultData);
71
+ }
72
+ else if (mode === 'writer') {
73
+ res = await api.post('/content-tools/ai/generate', { prompt });
74
+ resultData = res.data.data.text;
75
+ }
76
+ else {
77
+ const tool = TOOLS.find(t => t.id === activeTool);
78
+ let payload = { content: prompt };
79
+ if (activeTool === 'improve') {
80
+ payload = { text: prompt, instruction: 'Make it more professional, clear, and concise. Improve grammar and flow.' };
81
+ }
82
+ else if (activeTool === 'meta') {
83
+ payload = { title: 'Content', content: prompt };
84
+ }
85
+ else if (activeTool === 'seo') {
86
+ payload = { title: prompt.split('\n')[0]?.substring(0, 60), content: prompt, description: prompt.substring(0, 160) };
87
+ }
88
+ else if (activeTool === 'alt') {
89
+ payload = { imageUrl: prompt };
90
+ }
91
+ res = await api.post(tool.endpoint, payload);
92
+ const d = res.data.data;
93
+ if (activeTool === 'seo')
94
+ resultData = d;
95
+ else if (activeTool === 'quality')
96
+ resultData = d;
97
+ else if (activeTool === 'improve')
98
+ resultData = d.text;
99
+ else if (activeTool === 'meta')
100
+ resultData = d.description;
101
+ else if (activeTool === 'alt')
102
+ resultData = d.altText;
103
+ }
104
+ setResult(resultData);
105
+ setHistory(prev => [{
106
+ id: Date.now().toString(),
107
+ mode,
108
+ prompt: prompt.substring(0, 80),
109
+ result: resultData,
110
+ timestamp: new Date()
111
+ }, ...prev.slice(0, 19)]);
112
+ toast.success('Generated');
113
+ }
114
+ catch (err) {
115
+ const msg = err?.response?.data?.error?.message || err?.response?.data?.message || 'AI request failed';
116
+ toast.error(msg);
117
+ }
118
+ finally {
119
+ setLoading(false);
120
+ }
121
+ };
122
+ const handleKeyDown = (e) => {
123
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter')
124
+ handleExecute();
125
+ };
126
+ const saveSchemaToDb = async () => {
127
+ if (!schemaForSave)
128
+ return;
129
+ setSavingSchema(true);
130
+ try {
131
+ await api.post('/schemas', schemaForSave);
132
+ toast.success(`Collection "${schemaForSave.name}" created!`);
133
+ setSchemaForSave(null);
134
+ }
135
+ catch (err) {
136
+ toast.error(err?.response?.data?.error?.message || 'Failed to save schema');
137
+ }
138
+ finally {
139
+ setSavingSchema(false);
140
+ }
141
+ };
142
+ const copyResult = () => {
143
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
144
+ navigator.clipboard.writeText(text);
145
+ toast.success('Copied to clipboard');
146
+ };
147
+ const downloadResult = () => {
148
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
149
+ const ext = mode === 'architect' ? 'json' : 'txt';
150
+ const blob = new Blob([text], { type: 'text/plain' });
151
+ const url = URL.createObjectURL(blob);
152
+ const a = document.createElement('a');
153
+ a.href = url;
154
+ a.download = `zenith-ai-output.${ext}`;
155
+ a.click();
156
+ URL.revokeObjectURL(url);
157
+ };
158
+ const renderResult = () => {
159
+ if (!result)
160
+ return null;
161
+ if (mode === 'architect')
162
+ return _jsx(SchemaResultView, { data: result });
163
+ if (mode === 'tools' && activeTool === 'seo')
164
+ return _jsx(SeoResultView, { data: result });
165
+ if (mode === 'tools' && activeTool === 'quality')
166
+ return _jsx(QualityResultView, { data: result });
167
+ return (_jsx(motion.div, { initial: { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, className: "whitespace-pre-wrap text-[12px] leading-relaxed text-gray-300 font-sans", children: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }));
168
+ };
169
+ const activePlaceholder = mode === 'architect'
170
+ ? 'Describe a collection schema... e.g., "An e-commerce product with variants, pricing, and inventory tracking"'
171
+ : mode === 'writer'
172
+ ? 'Describe the content you want to generate... e.g., "Write a compelling blog intro about sustainable fashion"'
173
+ : activeTool === 'alt'
174
+ ? 'Paste an image URL to generate alt text...'
175
+ : activeTool === 'meta'
176
+ ? 'Paste your content to generate a meta description...'
177
+ : 'Paste text to analyze...';
178
+ return (_jsxs("div", { className: "flex flex-col h-[calc(100vh-64px)] overflow-hidden", children: [_jsx(PageHeader, { title: "AI Architect", description: "Generate content, design schemas, and analyze text quality", actions: _jsx("div", { className: cn('flex p-0.5 border', dark ? 'bg-black border-z-border' : 'bg-z-panel border-z-border'), children: MODES.map(m => (_jsxs("button", { onClick: () => { setMode(m.id); setResult(null); setSchemaForSave(null); }, title: m.desc, className: cn('flex items-center gap-2 px-4 py-2 text-[9px] font-black uppercase tracking-widest transition-all', mode === m.id
179
+ ? (dark ? 'bg-white text-black' : 'bg-gray-900 text-white')
180
+ : (dark ? 'text-z-secondary hover:text-white' : 'text-z-secondary hover:text-z-primary')), children: [_jsx(m.icon, { size: 11 }), _jsx("span", { className: "hidden sm:inline", children: m.label })] }, m.id))) }) }), _jsxs("div", { className: "flex flex-1 overflow-hidden", children: [_jsxs("div", { className: cn('w-56 flex-shrink-0 border-r flex flex-col hidden lg:flex', dark ? 'border-z-border bg-black' : 'border-z-border bg-gray-50'), children: [_jsx("div", { className: cn('px-4 py-3 border-b flex-shrink-0', dark ? 'border-z-border' : 'border-z-border'), children: _jsx("p", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-secondary", children: "History" }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-2 space-y-1", children: [history.length === 0 && (_jsx("p", { className: "text-[8px] text-gray-600 uppercase tracking-widest p-2 text-center mt-4", children: "No history yet" })), history.map(h => (_jsxs("button", { onClick: () => { setResult(h.result); setSchemaForSave(h.mode === 'architect' ? h.result : null); }, className: cn('w-full text-left p-2.5 border border-transparent transition-all', dark ? 'hover:bg-z-hover hover:border-z-border' : 'hover:bg-white hover:border-z-border'), children: [_jsx("div", { className: "flex items-center gap-1.5 mb-1", children: _jsx("span", { className: cn('text-[7px] font-black uppercase tracking-widest', h.mode === 'architect' ? 'text-purple-400' : h.mode === 'tools' ? 'text-amber-400' : 'text-z-active-text'), children: h.mode }) }), _jsx("p", { className: "text-[9px] text-z-secondary truncate", children: h.prompt })] }, h.id)))] })] }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0 border-r", style: { borderColor: dark ? 'rgba(255,255,255,0.08)' : '#e5e7eb' }, children: [_jsx(AnimatePresence, { children: mode === 'tools' && (_jsx(motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: 'auto', opacity: 1 }, exit: { height: 0, opacity: 0 }, className: cn('flex-shrink-0 border-b overflow-hidden', dark ? 'border-z-border' : 'border-z-border'), children: _jsx("div", { className: "p-3 flex gap-2 flex-wrap", children: TOOLS.map(tool => (_jsxs("button", { onClick: () => { setActiveTool(tool.id); setResult(null); }, title: tool.desc, className: cn('flex items-center gap-2 px-3 py-2 text-[9px] font-black uppercase tracking-widest border transition-all', activeTool === tool.id
181
+ ? (dark ? 'bg-white text-black border-white' : 'bg-gray-900 text-white border-gray-900')
182
+ : (dark ? 'border-z-border text-z-secondary hover:text-white hover:border-white/20' : 'border-z-border text-z-secondary hover:border-gray-400 hover:text-z-primary')), children: [_jsx(tool.icon, { size: 11, className: activeTool === tool.id ? '' : tool.color }), tool.name] }, tool.id))) }) })) }), _jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [_jsxs("div", { className: cn('flex-shrink-0 flex items-center gap-3 px-5 py-3 border-b', dark ? 'border-z-border' : 'border-z-border'), children: [_jsx(Terminal, { size: 12, className: "text-z-secondary" }), _jsx("span", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-secondary", children: mode === 'architect' ? 'Schema Prompt' : mode === 'writer' ? 'Content Prompt' : TOOLS.find(t => t.id === activeTool)?.name }), _jsx("span", { className: "ml-auto text-[8px] text-gray-600 uppercase tracking-widest hidden sm:block", children: "\u2318 + Enter to run" })] }), _jsx("textarea", { ref: textareaRef, value: prompt, onChange: e => setPrompt(e.target.value), onKeyDown: handleKeyDown, placeholder: activePlaceholder, className: cn('flex-1 w-full p-5 text-sm outline-none resize-none font-sans leading-relaxed', dark
183
+ ? 'bg-transparent text-white placeholder:text-gray-700'
184
+ : 'bg-transparent text-z-primary placeholder:text-z-muted') }), _jsx("div", { className: cn('flex-shrink-0 p-4 border-t', dark ? 'border-z-border' : 'border-z-border'), children: _jsxs("div", { className: "flex items-center gap-3", children: [prompt.trim() && (_jsx("button", { onClick: () => { setPrompt(''); setResult(null); }, className: "p-2.5 text-gray-600 hover:text-white transition-colors border border-transparent hover:border-white/10", title: "Clear", children: _jsx(RotateCcw, { size: 14 }) })), _jsx("button", { onClick: handleExecute, disabled: loading || (!prompt.trim() && activeTool !== 'alt'), className: cn('flex-1 py-3 font-black uppercase tracking-widest text-[10px] transition-all flex items-center justify-center gap-2.5', 'bg-z-accent hover:opacity-90 text-white', 'disabled:opacity-40 disabled:cursor-not-allowed', 'shadow-sm hover:shadow-sm'), children: loading
185
+ ? _jsxs(_Fragment, { children: [_jsx(Loader2, { size: 13, className: "animate-spin" }), " Generating\u2026"] })
186
+ : _jsxs(_Fragment, { children: [_jsx(Send, { size: 13 }), " ", mode === 'architect' ? 'Design Schema' : mode === 'writer' ? 'Generate Content' : 'Analyze'] }) })] }) })] })] }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("div", { className: cn('flex-shrink-0 flex items-center justify-between px-5 py-3 border-b', dark ? 'border-z-border' : 'border-z-border'), children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx(Sparkles, { size: 12, className: result ? 'text-z-active-text' : 'text-z-secondary' }), _jsx("span", { className: "text-[8px] font-black uppercase tracking-[0.3em] text-z-secondary", children: "Output" }), result && _jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-z-accent shadow-sm" })] }), result && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { onClick: copyResult, title: "Copy", className: "p-2 text-z-secondary hover:text-white transition-colors border border-transparent hover:border-white/10", children: _jsx(Copy, { size: 13 }) }), _jsx("button", { onClick: downloadResult, title: "Download", className: "p-2 text-z-secondary hover:text-white transition-colors border border-transparent hover:border-white/10", children: _jsx(Download, { size: 13 }) }), mode === 'architect' && (_jsx("button", { onClick: () => { setResult(null); setSchemaForSave(null); }, title: "Clear", className: "p-2 text-z-secondary hover:text-white transition-colors border border-transparent hover:border-white/10", children: _jsx(RotateCcw, { size: 13 }) }))] }))] }), _jsx("div", { className: "flex-1 overflow-auto p-6", children: loading ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center gap-4", children: [_jsxs("div", { className: "relative", children: [_jsx(Loader2, { size: 32, className: "animate-spin text-z-active-text" }), _jsx("div", { className: "absolute inset-0 blur-xl bg-z-accent/20 animate-pulse" })] }), _jsx("p", { className: "text-[9px] font-black uppercase tracking-[0.4em] text-z-secondary animate-pulse", children: mode === 'architect' ? 'Designing Schema…' : mode === 'writer' ? 'Writing Content…' : 'Analyzing…' })] })) : !result ? (_jsxs("div", { className: "h-full flex flex-col items-center justify-center gap-5 opacity-30", children: [_jsx(Cpu, { size: 40, className: "text-z-secondary" }), _jsxs("div", { className: "text-center space-y-1", children: [_jsx("p", { className: "text-[9px] font-black uppercase tracking-[0.4em] text-z-secondary", children: "Awaiting Input" }), _jsx("p", { className: "text-[8px] text-gray-600 uppercase tracking-widest", children: mode === 'architect' ? 'Describe a collection to generate a schema' : mode === 'writer' ? 'Write a prompt to generate content' : 'Paste content to analyze' })] })] })) : (_jsx(AnimatePresence, { mode: "wait", children: _jsx(motion.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.2 }, children: renderResult() }, JSON.stringify(result).substring(0, 50)) })) }), _jsx(AnimatePresence, { children: result && mode === 'architect' && schemaForSave && (_jsx(motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: 'auto', opacity: 1 }, exit: { height: 0, opacity: 0 }, className: cn('flex-shrink-0 border-t overflow-hidden', dark ? 'border-z-border' : 'border-z-border'), children: _jsxs("div", { className: "px-5 py-3 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-[8px] text-z-secondary uppercase tracking-widest", children: "Schema looks good? Save it as a live collection." }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { onClick: () => {
187
+ const text = JSON.stringify(schemaForSave, null, 2);
188
+ navigator.clipboard.writeText(text);
189
+ toast.success('Schema copied as JSON');
190
+ }, className: cn('px-4 py-2 border text-[9px] font-black uppercase tracking-widest flex items-center gap-2 transition-all', dark ? 'border-z-border text-z-muted hover:text-white hover:border-white/20' : 'border-z-border text-z-secondary hover:border-gray-400'), children: [_jsx(Code2, { size: 11 }), " Copy JSON"] }), _jsxs("button", { onClick: saveSchemaToDb, disabled: savingSchema, className: "px-5 py-2 bg-z-accent hover:opacity-90 text-white text-[9px] font-black uppercase tracking-widest flex items-center gap-2 transition-all disabled:opacity-50 shadow-sm", children: [savingSchema ? _jsx(Loader2, { size: 11, className: "animate-spin" }) : _jsx(Save, { size: 11 }), "Save Collection"] })] })] }) })) })] })] })] }));
191
+ };
192
+ export default AIWriterPage;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface SettingsAiProps {
3
+ settings: Record<string, any>;
4
+ setSettings: (s: any) => void;
5
+ theme: 'light' | 'dark';
6
+ }
7
+ declare const SettingsAi: React.FC<SettingsAiProps>;
8
+ export default SettingsAi;
@@ -0,0 +1,245 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Lock, Loader2, CheckCircle2, AlertCircle, ExternalLink, Eye, EyeOff, Cpu, Zap, ChevronRight, Info, TestTube2 } from 'lucide-react';
4
+ import { cn } from '../../admin/src/lib/utils';
5
+ import api from '../../admin/src/lib/api';
6
+ import toast from 'react-hot-toast';
7
+ const PROVIDERS = [
8
+ {
9
+ id: 'openrouter',
10
+ name: 'OpenRouter',
11
+ color: 'text-z-active-text',
12
+ description: 'Unified gateway to 200+ models from any provider via one API key',
13
+ docsUrl: 'https://openrouter.ai/keys',
14
+ keyPlaceholder: 'sk-or-v1-...',
15
+ keyField: 'openRouterApiKey',
16
+ badge: 'Recommended',
17
+ models: [
18
+ { value: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet', tier: 'pro' },
19
+ { value: 'anthropic/claude-3.5-haiku', label: 'Claude 3.5 Haiku', tier: 'pro' },
20
+ { value: 'anthropic/claude-3-opus', label: 'Claude 3 Opus', tier: 'ultra' },
21
+ { value: 'openai/gpt-4o', label: 'GPT-4o', tier: 'pro' },
22
+ { value: 'openai/gpt-4o-mini', label: 'GPT-4o Mini', tier: 'free' },
23
+ { value: 'openai/gpt-4-turbo', label: 'GPT-4 Turbo', tier: 'pro' },
24
+ { value: 'google/gemini-pro-1.5', label: 'Gemini 1.5 Pro', tier: 'pro' },
25
+ { value: 'google/gemini-flash-1.5', label: 'Gemini 1.5 Flash', tier: 'free' },
26
+ { value: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B', tier: 'free' },
27
+ { value: 'mistralai/mistral-large', label: 'Mistral Large', tier: 'pro' },
28
+ { value: 'mistralai/mixtral-8x7b-instruct', label: 'Mixtral 8x7B', tier: 'free' },
29
+ { value: 'deepseek/deepseek-r1', label: 'DeepSeek R1', tier: 'pro' },
30
+ { value: 'x-ai/grok-beta', label: 'Grok Beta', tier: 'pro' },
31
+ { value: 'cohere/command-r-plus', label: 'Cohere Command R+', tier: 'pro' },
32
+ { value: 'perplexity/llama-3.1-sonar-large-128k-online', label: 'Perplexity Sonar Large', tier: 'pro' },
33
+ ],
34
+ },
35
+ {
36
+ id: 'openai',
37
+ name: 'OpenAI',
38
+ color: 'text-z-active-text',
39
+ description: 'Direct access to GPT-4o, o1, and all OpenAI models',
40
+ docsUrl: 'https://platform.openai.com/api-keys',
41
+ keyPlaceholder: 'sk-proj-...',
42
+ keyField: 'openaiApiKey',
43
+ models: [
44
+ { value: 'gpt-4o', label: 'GPT-4o', tier: 'pro' },
45
+ { value: 'gpt-4o-mini', label: 'GPT-4o Mini', tier: 'free' },
46
+ { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', tier: 'pro' },
47
+ { value: 'o1-preview', label: 'o1 Preview', tier: 'ultra' },
48
+ { value: 'o1-mini', label: 'o1 Mini', tier: 'pro' },
49
+ { value: 'o3-mini', label: 'o3 Mini', tier: 'pro' },
50
+ ],
51
+ },
52
+ {
53
+ id: 'anthropic',
54
+ name: 'Anthropic',
55
+ color: 'text-orange-400',
56
+ description: 'Direct access to Claude models with vision and context support',
57
+ docsUrl: 'https://console.anthropic.com/settings/keys',
58
+ keyPlaceholder: 'sk-ant-...',
59
+ keyField: 'anthropicApiKey',
60
+ models: [
61
+ { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', tier: 'pro' },
62
+ { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku', tier: 'free' },
63
+ { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', tier: 'ultra' },
64
+ ],
65
+ },
66
+ {
67
+ id: 'google',
68
+ name: 'Google Gemini',
69
+ color: 'text-z-active-text',
70
+ description: 'Gemini Pro/Flash with long context and multimodal capabilities',
71
+ docsUrl: 'https://aistudio.google.com/app/apikey',
72
+ keyPlaceholder: 'AIza...',
73
+ keyField: 'googleApiKey',
74
+ models: [
75
+ { value: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', tier: 'pro' },
76
+ { value: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', tier: 'free' },
77
+ { value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash', tier: 'pro' },
78
+ ],
79
+ },
80
+ {
81
+ id: 'groq',
82
+ name: 'Groq',
83
+ color: 'text-pink-400',
84
+ description: 'Ultra-fast inference with LPU hardware — 800+ tokens/sec',
85
+ docsUrl: 'https://console.groq.com/keys',
86
+ keyPlaceholder: 'gsk_...',
87
+ keyField: 'groqApiKey',
88
+ badge: 'Fastest',
89
+ models: [
90
+ { value: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70B', tier: 'free' },
91
+ { value: 'llama-3.1-8b-instant', label: 'Llama 3.1 8B Instant', tier: 'free' },
92
+ { value: 'mixtral-8x7b-32768', label: 'Mixtral 8x7B', tier: 'free' },
93
+ { value: 'gemma2-9b-it', label: 'Gemma 2 9B', tier: 'free' },
94
+ ],
95
+ },
96
+ {
97
+ id: 'nvidia',
98
+ name: 'NVIDIA NIM',
99
+ color: 'text-green-400',
100
+ description: 'NVIDIA-hosted models with GPU-accelerated inference',
101
+ docsUrl: 'https://build.nvidia.com/explore/discover',
102
+ keyPlaceholder: 'nvapi-...',
103
+ keyField: 'nvidiaApiKey',
104
+ models: [
105
+ { value: 'meta/llama-3.1-405b-instruct', label: 'Llama 3.1 405B', tier: 'ultra' },
106
+ { value: 'meta/llama-3.1-70b-instruct', label: 'Llama 3.1 70B', tier: 'pro' },
107
+ { value: 'meta/llama-3.1-8b-instruct', label: 'Llama 3.1 8B', tier: 'free' },
108
+ { value: 'mistralai/mistral-large-2-instruct', label: 'Mistral Large 2', tier: 'pro' },
109
+ { value: 'nvidia/llama-3.1-nemotron-70b-instruct', label: 'Nemotron 70B', tier: 'pro' },
110
+ ],
111
+ },
112
+ {
113
+ id: 'together',
114
+ name: 'Together AI',
115
+ color: 'text-yellow-400',
116
+ description: 'Open-source models on fast distributed inference infrastructure',
117
+ docsUrl: 'https://api.together.xyz/settings/api-keys',
118
+ keyPlaceholder: 'together-...',
119
+ keyField: 'togetherApiKey',
120
+ models: [
121
+ { value: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', label: 'Llama 3.3 70B Turbo', tier: 'pro' },
122
+ { value: 'mistralai/Mixtral-8x7B-Instruct-v0.1', label: 'Mixtral 8x7B', tier: 'free' },
123
+ { value: 'deepseek-ai/DeepSeek-R1', label: 'DeepSeek R1', tier: 'pro' },
124
+ ],
125
+ },
126
+ {
127
+ id: 'mistral',
128
+ name: 'Mistral AI',
129
+ color: 'text-amber-400',
130
+ description: 'Direct access to Mistral, Codestral, and Pixtral models',
131
+ docsUrl: 'https://console.mistral.ai/api-keys',
132
+ keyPlaceholder: 'mistral-...',
133
+ keyField: 'mistralApiKey',
134
+ models: [
135
+ { value: 'mistral-large-latest', label: 'Mistral Large', tier: 'pro' },
136
+ { value: 'mistral-small-latest', label: 'Mistral Small', tier: 'free' },
137
+ { value: 'codestral-latest', label: 'Codestral', tier: 'pro' },
138
+ { value: 'pixtral-large-latest', label: 'Pixtral Large', tier: 'ultra' },
139
+ ],
140
+ },
141
+ {
142
+ id: 'cohere',
143
+ name: 'Cohere',
144
+ color: 'text-teal-400',
145
+ description: 'Enterprise-grade models optimized for search and RAG workflows',
146
+ docsUrl: 'https://dashboard.cohere.com/api-keys',
147
+ keyPlaceholder: 'co_...',
148
+ keyField: 'cohereApiKey',
149
+ models: [
150
+ { value: 'command-r-plus', label: 'Command R+', tier: 'ultra' },
151
+ { value: 'command-r', label: 'Command R', tier: 'pro' },
152
+ { value: 'command-light', label: 'Command Light', tier: 'free' },
153
+ ],
154
+ },
155
+ {
156
+ id: 'xai',
157
+ name: 'xAI / Grok',
158
+ color: 'text-gray-300',
159
+ description: "Elon Musk's xAI Grok model with real-time X/Twitter integration",
160
+ docsUrl: 'https://console.x.ai/',
161
+ keyPlaceholder: 'xai-...',
162
+ keyField: 'xaiApiKey',
163
+ models: [
164
+ { value: 'grok-beta', label: 'Grok Beta', tier: 'pro' },
165
+ { value: 'grok-vision-beta', label: 'Grok Vision Beta', tier: 'pro' },
166
+ ],
167
+ },
168
+ ];
169
+ const TIER_BADGE = {
170
+ free: 'bg-z-active-bg text-z-active-text border-z-accent/20',
171
+ pro: 'bg-z-accent/10 text-z-active-text border-z-accent/20',
172
+ ultra: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
173
+ };
174
+ const SettingsAi = ({ settings, setSettings, theme }) => {
175
+ const dark = theme === 'dark';
176
+ const [validating, setValidating] = useState(false);
177
+ const [testResult, setTestResult] = useState(null);
178
+ const [showKeys, setShowKeys] = useState({});
179
+ const [expandedProvider, setExpandedProvider] = useState('openrouter');
180
+ const [dynamicModels, setDynamicModels] = useState({});
181
+ const [fetchingModels, setFetchingModels] = useState(null);
182
+ const activeProvider = PROVIDERS.find(p => {
183
+ const key = settings[p.keyField]?.trim();
184
+ return key && key !== '[MASKED_CREDENTIAL]';
185
+ }) || PROVIDERS.find(p => p.id === 'openrouter');
186
+ const handleValidate = async () => {
187
+ setValidating(true);
188
+ setTestResult(null);
189
+ try {
190
+ const providerId = settings.aiProvider || 'openrouter';
191
+ const providerConfig = PROVIDERS.find(p => p.id === providerId);
192
+ const apiKeyField = providerConfig ? providerConfig.keyField : 'openRouterApiKey';
193
+ const apiKey = settings[apiKeyField];
194
+ const res = await api.post('/system/settings/ai/validate', {
195
+ provider: providerId,
196
+ model: settings.aiModel,
197
+ apiKey: apiKey
198
+ });
199
+ setTestResult({ ok: true, msg: res.data.message || 'API Key is valid' });
200
+ toast.success('AI connection verified');
201
+ }
202
+ catch (err) {
203
+ const msg = err?.response?.data?.error?.message || err?.response?.data?.message || 'Connection failed';
204
+ setTestResult({ ok: false, msg });
205
+ toast.error('AI connection failed');
206
+ }
207
+ finally {
208
+ setValidating(false);
209
+ }
210
+ };
211
+ const handleFetchModels = async (providerId, apiKeyField) => {
212
+ setFetchingModels(providerId);
213
+ try {
214
+ const apiKey = settings[apiKeyField];
215
+ const res = await api.post('/system/settings/ai/models', {
216
+ provider: providerId,
217
+ apiKey: apiKey
218
+ });
219
+ const models = res.data?.data || [];
220
+ setDynamicModels(prev => ({ ...prev, [providerId]: models }));
221
+ toast.success(`Fetched ${models.length} models for ${providerId}`);
222
+ }
223
+ catch (err) {
224
+ const msg = err?.response?.data?.error?.message || err?.response?.data?.message || 'Failed to fetch models';
225
+ toast.error(msg);
226
+ }
227
+ finally {
228
+ setFetchingModels(null);
229
+ }
230
+ };
231
+ const toggleKey = (id) => setShowKeys(prev => ({ ...prev, [id]: !prev[id] }));
232
+ const inp = (dark) => cn('w-full border px-3 py-2.5 text-sm font-semibold outline-none transition-colors rounded-none focus-visible:ring-2 focus-visible:ring-z-active-border focus-visible:ring-offset-1 focus-visible:ring-offset-black', dark
233
+ ? 'bg-black border-z-border text-white placeholder:text-gray-700 focus:border-z-accent'
234
+ : 'bg-z-panel border-z-border text-z-primary placeholder:text-z-muted focus:border-z-accent');
235
+ return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: cn('p-5 border space-y-4 shadow-sm', dark ? 'bg-z-panel backdrop-blur-md border-z-border' : 'bg-z-input border-z-border'), children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx(Cpu, { size: 14, className: "text-z-active-text" }), _jsx("span", { className: cn('text-sm font-semibold ', dark ? 'text-white' : 'text-z-primary'), children: "Active Model" }), _jsx("span", { className: "ml-auto text-sm text-z-secondary", children: "Used by all AI features" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-sm font-semibold text-z-secondary", children: "AI Provider" }), _jsx("select", { value: settings.aiProvider || 'openrouter', onChange: e => setSettings({ ...settings, aiProvider: e.target.value, aiModel: PROVIDERS.find(p => p.id === e.target.value)?.models[0]?.value || '' }), className: inp(dark), children: PROVIDERS.map(p => _jsx("option", { value: p.id, children: p.name }, p.id)) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-sm font-semibold text-z-secondary", children: "Model" }), _jsx("select", { value: settings.aiModel || '', onChange: e => setSettings({ ...settings, aiModel: e.target.value }), className: inp(dark), children: (dynamicModels[settings.aiProvider || 'openrouter'] || PROVIDERS.find(p => p.id === (settings.aiProvider || 'openrouter'))?.models || []).map(m => (_jsxs("option", { value: m.value, children: [m.label, " ", m.tier ? `(${m.tier})` : ''] }, m.value))) })] })] }), _jsxs("div", { className: "flex items-center gap-3 pt-1", children: [_jsxs("button", { onClick: handleValidate, disabled: validating, className: "px-4 py-2 bg-z-accent hover:opacity-90 shadow-sm text-white text-sm font-semibold flex items-center gap-2 disabled:opacity-50 transition-all", children: [validating ? _jsx(Loader2, { size: 11, className: "animate-spin" }) : _jsx(TestTube2, { size: 11 }), "Test Connection"] }), testResult && (_jsxs("div", { className: cn('flex items-center gap-2 text-sm font-semibold ', testResult.ok ? 'text-z-active-text' : 'text-red-400'), children: [testResult.ok ? _jsx(CheckCircle2, { size: 11 }) : _jsx(AlertCircle, { size: 11 }), testResult.msg] }))] })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx(Lock, { size: 12, className: "text-z-secondary" }), _jsx("span", { className: "text-sm font-semibold text-z-secondary", children: "Provider API Keys" }), _jsx("span", { className: "ml-auto text-sm text-gray-600", children: "All keys encrypted at rest" })] }), PROVIDERS.map(provider => {
236
+ const isExpanded = expandedProvider === provider.id;
237
+ const keyValue = settings[provider.keyField] || '';
238
+ const hasKey = keyValue.trim() && keyValue !== '[MASKED_CREDENTIAL]';
239
+ const isMasked = keyValue === '[MASKED_CREDENTIAL]';
240
+ return (_jsxs("div", { className: cn('border transition-all shadow-sm', isExpanded
241
+ ? (dark ? 'border-white/15 bg-black/80 backdrop-blur-md shadow-sm' : 'border-z-border-strong bg-white')
242
+ : ('z-card-interactive')), children: [_jsxs("button", { onClick: () => setExpandedProvider(isExpanded ? '' : provider.id), className: "w-full flex items-center gap-3 px-4 py-3 text-left", children: [_jsxs("div", { className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsx("div", { className: cn('w-2 h-2 rounded-full flex-shrink-0', hasKey || isMasked ? 'bg-z-accent shadow-sm' : 'bg-gray-700') }), _jsx("div", { className: cn('text-sm font-semibold', provider.color), children: provider.name }), provider.badge && (_jsx("span", { className: "text-sm font-semibold px-1.5 py-0.5 bg-z-active-bg border border-z-active-border text-z-active-text", children: provider.badge })), _jsx("span", { className: "text-sm text-gray-600 truncate hidden sm:block", children: provider.description })] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [isMasked && _jsx("span", { className: "text-sm text-z-active-text font-semibold", children: "Configured" }), hasKey && !isMasked && _jsx("span", { className: "text-sm text-z-active-text font-semibold", children: "Active" }), _jsx(ChevronRight, { size: 12, className: cn('text-z-secondary transition-transform', isExpanded && 'rotate-90') })] })] }), isExpanded && (_jsxs("div", { className: "px-4 pb-4 space-y-3 border-t", style: { borderColor: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)' }, children: [_jsx("p", { className: "text-sm text-z-secondary pt-3", children: provider.description }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { className: "text-sm font-semibold text-z-secondary", children: "API Key" }), _jsxs("a", { href: provider.docsUrl, target: "_blank", rel: "noopener noreferrer", className: "text-sm text-z-active-text hover:text-z-active-text flex items-center gap-1 transition-colors", children: ["Get Key ", _jsx(ExternalLink, { size: 9 })] })] }), _jsxs("div", { className: "relative", children: [_jsx("input", { type: showKeys[provider.id] ? 'text' : 'password', value: keyValue, onChange: e => setSettings({ ...settings, [provider.keyField]: e.target.value }), placeholder: isMasked ? '••••••••••••••••' : provider.keyPlaceholder, className: cn(inp(dark), 'pr-10 font-mono') }), _jsx("button", { type: "button", onClick: () => toggleKey(provider.id), className: "absolute right-3 top-1/2 -translate-y-1/2 text-z-secondary hover:text-white transition-colors", children: showKeys[provider.id] ? _jsx(EyeOff, { size: 13 }) : _jsx(Eye, { size: 13 }) })] }), isMasked && (_jsxs("p", { className: "text-sm text-amber-500/70 flex items-center gap-1", children: [_jsx(Lock, { size: 9 }), " Key is stored \u2014 enter a new value to replace it"] }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("label", { className: "text-sm font-semibold text-z-secondary", children: ["Available Models ", dynamicModels[provider.id] ? `(${dynamicModels[provider.id].length})` : ''] }), _jsxs("button", { type: "button", onClick: () => handleFetchModels(provider.id, provider.keyField), disabled: fetchingModels === provider.id || (!hasKey && !isMasked), className: "text-sm text-z-active-text hover:text-z-active-text flex items-center gap-1 disabled:opacity-50 transition-colors", children: [fetchingModels === provider.id ? _jsx(Loader2, { size: 9, className: "animate-spin" }) : _jsx(Zap, { size: 9 }), "Fetch Models"] })] }), _jsx("div", { className: "flex flex-wrap gap-1.5 max-h-[120px] overflow-y-auto pr-2 custom-scrollbar", children: (dynamicModels[provider.id] || provider.models).map(m => (_jsx("span", { title: m.value, className: cn('text-sm font-semibold px-2 py-1 border', m.tier ? TIER_BADGE[m.tier] : 'bg-z-hover border-white/10 text-z-muted'), children: m.label }, m.value))) })] })] }))] }, provider.id));
243
+ })] }), _jsxs("div", { className: cn('flex gap-3 p-4 border', dark ? 'bg-z-accent/5 border-z-accent/15' : 'bg-z-active-bg border-z-active-border'), children: [_jsx(Info, { size: 12, className: "text-z-active-text flex-shrink-0 mt-0.5" }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-semibold text-z-active-text", children: "Provider Priority" }), _jsx("p", { className: "text-sm text-z-secondary leading-relaxed", children: "The AI engine auto-selects providers in this order: OpenRouter \u2192 xAI \u2192 NVIDIA NIM \u2192 Groq \u2192 Together AI \u2192 Mistral \u2192 Cohere \u2192 OpenAI \u2192 Anthropic \u2192 Google Gemini. Set the \"Active Model\" above to override. Keys are never sent to the client." })] })] })] }));
244
+ };
245
+ export default SettingsAi;
@@ -0,0 +1 @@
1
+ export * from './plugin';
@@ -0,0 +1 @@
1
+ export * from './plugin';
@@ -0,0 +1,2 @@
1
+ export { default as AIWriterPage } from './AIWriterPage';
2
+ export { default as SettingsAi } from './SettingsAi';
@@ -0,0 +1,2 @@
1
+ export { default as AIWriterPage } from './AIWriterPage';
2
+ export { default as SettingsAi } from './SettingsAi';
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@zenith-open/zenithcms-plugin-ai-architect-ui",
3
+ "version": "1.0.0-beta.10",
4
+ "type": "module",
5
+ "description": "AI Copilot and SEO Architect UI Plugin for Zenith CMS",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "dependencies": {
9
+ "framer-motion": "^11.0.0",
10
+ "lucide-react": "^0.354.0",
11
+ "react-hot-toast": "2.6.0"
12
+ },
13
+ "devDependencies": {
14
+ "@types/react": "^19.2.14",
15
+ "@types/react-dom": "^19.2.3",
16
+ "react": "^19.2.5",
17
+ "react-dom": "^19.2.5",
18
+ "typescript": "^5.0.0",
19
+ "vite": "^5.4.21"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^18.2.0 || ^19.0.0",
23
+ "react-dom": "^18.2.0 || ^19.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc"
34
+ }
35
+ }