@zenith-open/zenithcms-plugin-ai-architect-ui 1.0.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.
- package/LICENSE +21 -0
- package/dist/AIWriterPage.d.ts +2 -0
- package/dist/AIWriterPage.js +192 -0
- package/dist/SettingsAi.d.ts +8 -0
- package/dist/SettingsAi.js +245 -0
- package/dist/admin/src/components/ui/PageHeader.d.ts +14 -0
- package/dist/admin/src/components/ui/PageHeader.js +7 -0
- package/dist/admin/src/context/ThemeContext.d.ts +12 -0
- package/dist/admin/src/context/ThemeContext.js +25 -0
- package/dist/admin/src/lib/ApiError.d.ts +41 -0
- package/dist/admin/src/lib/ApiError.js +34 -0
- package/dist/admin/src/lib/api.d.ts +31 -0
- package/dist/admin/src/lib/api.js +240 -0
- package/dist/admin/src/lib/tenantStore.d.ts +31 -0
- package/dist/admin/src/lib/tenantStore.js +46 -0
- package/dist/admin/src/lib/utils.d.ts +15 -0
- package/dist/admin/src/lib/utils.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin-ai-architect-ui/src/AIWriterPage.d.ts +2 -0
- package/dist/plugin-ai-architect-ui/src/AIWriterPage.js +192 -0
- package/dist/plugin-ai-architect-ui/src/SettingsAi.d.ts +8 -0
- package/dist/plugin-ai-architect-ui/src/SettingsAi.js +245 -0
- package/dist/plugin-ai-architect-ui/src/index.d.ts +1 -0
- package/dist/plugin-ai-architect-ui/src/index.js +1 -0
- package/dist/plugin-ai-architect-ui/src/plugin.d.ts +2 -0
- package/dist/plugin-ai-architect-ui/src/plugin.js +2 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +2 -0
- package/package.json +34 -0
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,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 '../lib/api';
|
|
5
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
6
|
+
import { cn } from '../lib/utils';
|
|
7
|
+
import toast from 'react-hot-toast';
|
|
8
|
+
import { useTheme } from '../context/ThemeContext';
|
|
9
|
+
import { PageHeader } from '../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,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 '../../lib/utils';
|
|
5
|
+
import api from '../../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,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
|
+
}
|