@crmy/web 0.5.5 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-CskfWp8E.js +560 -0
- package/dist/assets/index-D763l57m.css +1 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +4 -1
- package/postcss.config.js +0 -6
- package/src/App.tsx +0 -158
- package/src/api/client.ts +0 -82
- package/src/api/hooks.ts +0 -689
- package/src/components/CustomFields.tsx +0 -240
- package/src/components/NavLink.tsx +0 -28
- package/src/components/crm/AIFab.tsx +0 -37
- package/src/components/crm/AccountDrawer.tsx +0 -372
- package/src/components/crm/ActivityTimeline.tsx +0 -115
- package/src/components/crm/AssignmentDrawer.tsx +0 -396
- package/src/components/crm/BriefingPanel.tsx +0 -217
- package/src/components/crm/CommandPalette.tsx +0 -254
- package/src/components/crm/ContactAvatar.tsx +0 -49
- package/src/components/crm/ContactDrawer.tsx +0 -438
- package/src/components/crm/ContextPanel.tsx +0 -200
- package/src/components/crm/CrmWidgets.tsx +0 -417
- package/src/components/crm/DrawerShell.tsx +0 -77
- package/src/components/crm/ListToolbar.tsx +0 -252
- package/src/components/crm/OpportunityDrawer.tsx +0 -372
- package/src/components/crm/PaginationBar.tsx +0 -111
- package/src/components/crm/QuickAddDrawer.tsx +0 -652
- package/src/components/crm/ShortcutsOverlay.tsx +0 -65
- package/src/components/crm/UseCaseDrawer.tsx +0 -454
- package/src/components/layout/MobileNav.tsx +0 -49
- package/src/components/layout/Sidebar.tsx +0 -157
- package/src/components/layout/TopBar.tsx +0 -54
- package/src/components/settings/ActorsSettings.tsx +0 -1190
- package/src/components/ui/accordion.tsx +0 -52
- package/src/components/ui/alert-dialog.tsx +0 -104
- package/src/components/ui/alert.tsx +0 -43
- package/src/components/ui/aspect-ratio.tsx +0 -5
- package/src/components/ui/avatar.tsx +0 -38
- package/src/components/ui/badge.tsx +0 -29
- package/src/components/ui/breadcrumb.tsx +0 -90
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/calendar.tsx +0 -54
- package/src/components/ui/card.tsx +0 -43
- package/src/components/ui/carousel.tsx +0 -224
- package/src/components/ui/chart.tsx +0 -303
- package/src/components/ui/checkbox.tsx +0 -26
- package/src/components/ui/collapsible.tsx +0 -9
- package/src/components/ui/command.tsx +0 -132
- package/src/components/ui/context-menu.tsx +0 -178
- package/src/components/ui/date-picker.tsx +0 -313
- package/src/components/ui/dialog.tsx +0 -95
- package/src/components/ui/drawer.tsx +0 -87
- package/src/components/ui/dropdown-menu.tsx +0 -179
- package/src/components/ui/form.tsx +0 -129
- package/src/components/ui/hover-card.tsx +0 -27
- package/src/components/ui/input-otp.tsx +0 -61
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -17
- package/src/components/ui/menubar.tsx +0 -207
- package/src/components/ui/navigation-menu.tsx +0 -120
- package/src/components/ui/pagination.tsx +0 -81
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -23
- package/src/components/ui/radio-group.tsx +0 -36
- package/src/components/ui/resizable.tsx +0 -37
- package/src/components/ui/scroll-area.tsx +0 -38
- package/src/components/ui/select.tsx +0 -143
- package/src/components/ui/separator.tsx +0 -20
- package/src/components/ui/sheet.tsx +0 -107
- package/src/components/ui/sidebar.tsx +0 -637
- package/src/components/ui/skeleton.tsx +0 -7
- package/src/components/ui/slider.tsx +0 -23
- package/src/components/ui/sonner.tsx +0 -24
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -72
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -21
- package/src/components/ui/toast.tsx +0 -111
- package/src/components/ui/toaster.tsx +0 -24
- package/src/components/ui/toggle-group.tsx +0 -49
- package/src/components/ui/toggle.tsx +0 -37
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/components/ui/use-toast.ts +0 -1
- package/src/components/ui/utils.ts +0 -9
- package/src/contexts/AgentSettingsContext.tsx +0 -24
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-toast.ts +0 -186
- package/src/hooks/useKeyboardShortcuts.ts +0 -95
- package/src/hooks/useTheme.ts +0 -24
- package/src/index.css +0 -245
- package/src/lib/entityColors.ts +0 -18
- package/src/lib/stageConfig.ts +0 -32
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -25
- package/src/pages/Accounts.tsx +0 -205
- package/src/pages/Activities.tsx +0 -251
- package/src/pages/Agent.tsx +0 -237
- package/src/pages/AgentSettings.tsx +0 -544
- package/src/pages/Assignments.tsx +0 -750
- package/src/pages/Contacts.tsx +0 -200
- package/src/pages/Dashboard.tsx +0 -143
- package/src/pages/Inbox.tsx +0 -615
- package/src/pages/NotFound.tsx +0 -24
- package/src/pages/Opportunities.tsx +0 -386
- package/src/pages/SearchResults.tsx +0 -49
- package/src/pages/Settings.tsx +0 -1884
- package/src/pages/UseCases.tsx +0 -396
- package/src/pages/auth/Login.tsx +0 -261
- package/src/pages/hitl/HITL.tsx +0 -101
- package/src/store/appStore.ts +0 -103
- package/src/vite-env.d.ts +0 -14
- package/tailwind.config.js +0 -121
- package/tsconfig.json +0 -24
- package/vite.config.ts +0 -27
- /package/{public → dist}/android-chrome-192x192.png +0 -0
- /package/{public → dist}/android-chrome-512x512.png +0 -0
- /package/{public → dist}/apple-touch-icon.png +0 -0
- /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
- /package/{public → dist}/favicon-16x16.png +0 -0
- /package/{public → dist}/favicon-32x32.png +0 -0
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/site.webmanifest +0 -0
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 CRMy Contributors
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
5
|
-
import { Sparkles, KeyRound, Users, TriangleAlert, Eye, EyeOff, X } from 'lucide-react';
|
|
6
|
-
import { Switch } from '@/components/ui/switch';
|
|
7
|
-
import { toast } from '@/hooks/use-toast';
|
|
8
|
-
import { useAgentSettings } from '@/contexts/AgentSettingsContext';
|
|
9
|
-
|
|
10
|
-
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
type Provider = 'anthropic' | 'openai' | 'openrouter' | 'ollama' | 'custom';
|
|
13
|
-
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const providerModels: Record<Provider, string[]> = {
|
|
16
|
-
anthropic: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'],
|
|
17
|
-
openai: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
|
|
18
|
-
openrouter: ['openrouter/auto', 'anthropic/claude-sonnet-4', 'openai/gpt-4o', 'google/gemini-2.0-flash'],
|
|
19
|
-
ollama: ['llama3.2', 'mistral', 'deepseek-r1'],
|
|
20
|
-
custom: [],
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const providerUrls: Record<Provider, string> = {
|
|
24
|
-
anthropic: 'https://api.anthropic.com/v1',
|
|
25
|
-
openai: 'https://api.openai.com/v1',
|
|
26
|
-
openrouter: 'https://openrouter.ai/api/v1',
|
|
27
|
-
ollama: 'http://localhost:11434/v1',
|
|
28
|
-
custom: 'https://your-gateway.example.com/v1',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const providerColors: Record<Provider, { dot: string; active: string }> = {
|
|
32
|
-
anthropic: { dot: 'bg-amber-500', active: 'border-amber-500/50 bg-amber-500/10 text-amber-600 dark:text-amber-400' },
|
|
33
|
-
openai: { dot: 'bg-green-500', active: 'border-green-500/50 bg-green-500/10 text-green-600 dark:text-green-400' },
|
|
34
|
-
openrouter: { dot: 'bg-purple-500', active: 'border-purple-500/50 bg-purple-500/10 text-purple-600 dark:text-purple-400' },
|
|
35
|
-
ollama: { dot: 'bg-blue-500', active: 'border-blue-500/50 bg-blue-500/10 text-blue-600 dark:text-blue-400' },
|
|
36
|
-
custom: { dot: 'bg-gray-400', active: 'border-gray-400/50 bg-gray-400/10 text-gray-600 dark:text-gray-400' },
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const tokenCosts: Record<number, string> = {
|
|
40
|
-
1000: '~$0.002 / turn at current model pricing',
|
|
41
|
-
4000: '~$0.006 / turn at current model pricing',
|
|
42
|
-
8000: '~$0.012 / turn at current model pricing',
|
|
43
|
-
16000: '~$0.024 / turn at current model pricing',
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const defaultSettings = {
|
|
47
|
-
provider: 'anthropic' as Provider,
|
|
48
|
-
baseUrl: 'https://api.anthropic.com/v1',
|
|
49
|
-
apiKey: 'sk-ant-api03-••••••••••••••••••••••••••••••••',
|
|
50
|
-
model: 'claude-sonnet-4-20250514',
|
|
51
|
-
historyRetentionDays: 90,
|
|
52
|
-
systemPrompt: `You are a CRMy AI agent helping [User Name] manage their real estate/mortgage/insurance pipeline. You have access to their contacts, accounts, opportunities, use cases, and activity history via MCP tools. Be concise, accurate, and always confirm before making changes to CRM objects.`,
|
|
53
|
-
maxTokensPerTurn: 4000,
|
|
54
|
-
canCreateAssignments: true,
|
|
55
|
-
canLogActivities: true,
|
|
56
|
-
canWriteObjects: false,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// ─── Shared style constants ───────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
const inputCls = 'w-full h-9 px-3 rounded-lg border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring';
|
|
62
|
-
const selectCls = `${inputCls} cursor-pointer`;
|
|
63
|
-
const primaryBtn = 'px-3 py-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:bg-primary/90 transition-colors disabled:opacity-40';
|
|
64
|
-
const ghostBtn = 'px-3 py-1.5 rounded-lg border border-border text-xs font-semibold text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors';
|
|
65
|
-
const dangerBtn = 'px-3 py-1.5 rounded-lg bg-destructive text-destructive-foreground text-xs font-semibold hover:bg-destructive/90 transition-colors';
|
|
66
|
-
|
|
67
|
-
// ─── Main Component ───────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
export default function AgentSettings() {
|
|
70
|
-
const { enabled, setEnabled } = useAgentSettings();
|
|
71
|
-
|
|
72
|
-
// Section 2 — Provider & Model
|
|
73
|
-
const [provider, setProvider] = useState<Provider>(defaultSettings.provider);
|
|
74
|
-
const [baseUrl, setBaseUrl] = useState(defaultSettings.baseUrl);
|
|
75
|
-
const [apiKey, setApiKey] = useState(defaultSettings.apiKey);
|
|
76
|
-
const [showApiKey, setShowApiKey] = useState(false);
|
|
77
|
-
const [model, setModel] = useState(defaultSettings.model);
|
|
78
|
-
const [customModel, setCustomModel] = useState('');
|
|
79
|
-
const [testingConn, setTestingConn] = useState(false);
|
|
80
|
-
const [savingProvider, setSavingProvider] = useState(false);
|
|
81
|
-
const [saveProviderDone, setSaveProviderDone] = useState(false);
|
|
82
|
-
|
|
83
|
-
// Section 3 — Per-User Behaviour
|
|
84
|
-
const [historyRetention, setHistoryRetention] = useState(defaultSettings.historyRetentionDays);
|
|
85
|
-
const [systemPrompt, setSystemPrompt] = useState(defaultSettings.systemPrompt);
|
|
86
|
-
const [editingPrompt, setEditingPrompt] = useState(false);
|
|
87
|
-
const [promptDraft, setPromptDraft] = useState(defaultSettings.systemPrompt);
|
|
88
|
-
const [maxTokens, setMaxTokens] = useState(defaultSettings.maxTokensPerTurn);
|
|
89
|
-
const [canCreateAssignments, setCanCreateAssignments] = useState(defaultSettings.canCreateAssignments);
|
|
90
|
-
const [canLogActivities, setCanLogActivities] = useState(defaultSettings.canLogActivities);
|
|
91
|
-
const [canWriteObjects, setCanWriteObjects] = useState(defaultSettings.canWriteObjects);
|
|
92
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
93
|
-
|
|
94
|
-
// Section 4 — Danger Zone
|
|
95
|
-
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
|
96
|
-
const [clearConfirmText, setClearConfirmText] = useState('');
|
|
97
|
-
|
|
98
|
-
// Focus textarea when prompt editor opens
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (editingPrompt && textareaRef.current) {
|
|
101
|
-
textareaRef.current.focus();
|
|
102
|
-
}
|
|
103
|
-
}, [editingPrompt]);
|
|
104
|
-
|
|
105
|
-
// Provider change — update URL and model
|
|
106
|
-
const handleProviderChange = (p: Provider) => {
|
|
107
|
-
setProvider(p);
|
|
108
|
-
setBaseUrl(providerUrls[p]);
|
|
109
|
-
const models = providerModels[p];
|
|
110
|
-
setModel(models[0] ?? '');
|
|
111
|
-
setCustomModel('');
|
|
112
|
-
setSaveProviderDone(false);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Test connection
|
|
116
|
-
const handleTestConnection = async () => {
|
|
117
|
-
setTestingConn(true);
|
|
118
|
-
await new Promise(r => setTimeout(r, 1200));
|
|
119
|
-
setTestingConn(false);
|
|
120
|
-
if (Math.random() > 0.3) {
|
|
121
|
-
toast({ title: 'Connection successful', description: `Connected to ${provider} at ${baseUrl}` });
|
|
122
|
-
} else {
|
|
123
|
-
toast({ title: 'Connection failed', description: 'Could not reach the provider. Check your API key and base URL.', variant: 'destructive' });
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Save provider settings
|
|
128
|
-
const handleSaveProvider = async () => {
|
|
129
|
-
setSavingProvider(true);
|
|
130
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
131
|
-
setSavingProvider(false);
|
|
132
|
-
setSaveProviderDone(true);
|
|
133
|
-
toast({ title: 'Settings saved', description: 'Provider configuration updated.' });
|
|
134
|
-
setTimeout(() => setSaveProviderDone(false), 3000);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Clear all histories
|
|
138
|
-
const handleClearAll = () => {
|
|
139
|
-
setShowClearConfirm(false);
|
|
140
|
-
setClearConfirmText('');
|
|
141
|
-
toast({ title: 'Chat histories cleared', description: 'All agent chat histories have been deleted.' });
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const dimCls = !enabled ? 'opacity-40 pointer-events-none' : '';
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<div className="space-y-5 max-w-2xl">
|
|
148
|
-
<div>
|
|
149
|
-
<h2 className="font-display font-bold text-lg text-foreground mb-1">Local AI Agent</h2>
|
|
150
|
-
<p className="text-sm text-muted-foreground">Configure the AI agent for your workspace.</p>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
{/* ── SECTION 1: Enable AI Agent ─────────────────────────────────────── */}
|
|
154
|
-
<div className="rounded-xl border border-border bg-card overflow-hidden">
|
|
155
|
-
{/* Header */}
|
|
156
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
157
|
-
<div className="flex items-center gap-3">
|
|
158
|
-
<div className="w-9 h-9 rounded-xl bg-amber-500/10 flex items-center justify-center">
|
|
159
|
-
<Sparkles className="w-4 h-4 text-amber-500" />
|
|
160
|
-
</div>
|
|
161
|
-
<div>
|
|
162
|
-
<h3 className="text-sm font-semibold text-foreground">Enable In-App AI agent</h3>
|
|
163
|
-
<p className="text-xs text-muted-foreground">Toggle AI features on or off for this workspace</p>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
<div className="flex items-center gap-3">
|
|
167
|
-
<span className="text-[11px] font-semibold px-2 py-0.5 rounded-full border bg-muted text-muted-foreground border-border">
|
|
168
|
-
Coming soon
|
|
169
|
-
</span>
|
|
170
|
-
<Switch
|
|
171
|
-
checked={false}
|
|
172
|
-
onCheckedChange={() => {}}
|
|
173
|
-
disabled
|
|
174
|
-
aria-label="Enable In-App AI agent"
|
|
175
|
-
/>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
{/* Info banner */}
|
|
180
|
-
<div className="px-5 py-3 bg-muted/40 border-b border-border">
|
|
181
|
-
<p className="text-xs text-muted-foreground">
|
|
182
|
-
Backend support for the AI agent is not yet available. This toggle will be enabled once the agent service is configured and deployed.
|
|
183
|
-
</p>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
{/* Feature tags */}
|
|
187
|
-
{enabled && (
|
|
188
|
-
<div className="px-5 py-3 flex flex-wrap gap-2">
|
|
189
|
-
{[
|
|
190
|
-
{ icon: '✦', label: 'Sparkle column (tables)' },
|
|
191
|
-
{ icon: '💬', label: 'Chat button (object detail)' },
|
|
192
|
-
{ icon: '◎', label: 'Floating AI icon' },
|
|
193
|
-
{ icon: '✎', label: 'AI-assist on edit' },
|
|
194
|
-
].map((tag) => (
|
|
195
|
-
<span key={tag.label} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded-lg bg-muted text-muted-foreground border border-border">
|
|
196
|
-
<span>{tag.icon}</span> {tag.label}
|
|
197
|
-
</span>
|
|
198
|
-
))}
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
{/* ── SECTION 2: Provider & Model ────────────────────────────────────── */}
|
|
204
|
-
<div className={`rounded-xl border border-border bg-card overflow-hidden transition-opacity ${dimCls}`} aria-disabled={!enabled}>
|
|
205
|
-
{/* Header */}
|
|
206
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
207
|
-
<div className="flex items-center gap-3">
|
|
208
|
-
<div className="w-9 h-9 rounded-xl bg-blue-500/10 flex items-center justify-center">
|
|
209
|
-
<KeyRound className="w-4 h-4 text-blue-500" />
|
|
210
|
-
</div>
|
|
211
|
-
<div>
|
|
212
|
-
<h3 className="text-sm font-semibold text-foreground">Provider & model</h3>
|
|
213
|
-
<p className="text-xs text-muted-foreground">Connect to an LLM gateway or direct provider</p>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
<div className="flex items-center gap-1.5">
|
|
217
|
-
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
218
|
-
<span className="text-xs text-muted-foreground">Connected</span>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
<div className="divide-y divide-border px-5">
|
|
223
|
-
{/* Provider pills */}
|
|
224
|
-
<div className="py-4 space-y-2">
|
|
225
|
-
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Provider</label>
|
|
226
|
-
<div className="flex flex-wrap gap-2">
|
|
227
|
-
{(['anthropic', 'openai', 'openrouter', 'ollama', 'custom'] as Provider[]).map((p) => {
|
|
228
|
-
const isActive = provider === p;
|
|
229
|
-
return (
|
|
230
|
-
<button
|
|
231
|
-
key={p}
|
|
232
|
-
onClick={() => handleProviderChange(p)}
|
|
233
|
-
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-semibold transition-colors ${isActive ? providerColors[p].active + ' border' : 'border-border text-muted-foreground hover:text-foreground bg-muted/40'}`}
|
|
234
|
-
>
|
|
235
|
-
<span className={`w-1.5 h-1.5 rounded-full ${providerColors[p].dot}`} />
|
|
236
|
-
{p.charAt(0).toUpperCase() + p.slice(1)}
|
|
237
|
-
</button>
|
|
238
|
-
);
|
|
239
|
-
})}
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
{/* Base URL */}
|
|
244
|
-
<div className="py-4 space-y-1.5">
|
|
245
|
-
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Base URL</label>
|
|
246
|
-
<input
|
|
247
|
-
value={baseUrl}
|
|
248
|
-
onChange={e => setBaseUrl(e.target.value)}
|
|
249
|
-
placeholder={providerUrls[provider]}
|
|
250
|
-
className={inputCls}
|
|
251
|
-
/>
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
{/* API Key */}
|
|
255
|
-
{provider !== 'ollama' ? (
|
|
256
|
-
<div className="py-4 space-y-1.5">
|
|
257
|
-
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">API Key</label>
|
|
258
|
-
<div className="relative">
|
|
259
|
-
<input
|
|
260
|
-
type={showApiKey ? 'text' : 'password'}
|
|
261
|
-
value={apiKey}
|
|
262
|
-
onChange={e => setApiKey(e.target.value)}
|
|
263
|
-
placeholder="sk-..."
|
|
264
|
-
className={inputCls + ' pr-10'}
|
|
265
|
-
/>
|
|
266
|
-
<button
|
|
267
|
-
type="button"
|
|
268
|
-
onClick={() => setShowApiKey(v => !v)}
|
|
269
|
-
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
270
|
-
>
|
|
271
|
-
{showApiKey ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
|
272
|
-
</button>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
) : (
|
|
276
|
-
<div className="py-4">
|
|
277
|
-
<p className="text-xs text-muted-foreground italic">Ollama runs locally — no API key required.</p>
|
|
278
|
-
</div>
|
|
279
|
-
)}
|
|
280
|
-
|
|
281
|
-
{/* Model */}
|
|
282
|
-
<div className="py-4 space-y-1.5">
|
|
283
|
-
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Model</label>
|
|
284
|
-
{provider === 'custom' ? (
|
|
285
|
-
<input
|
|
286
|
-
value={customModel}
|
|
287
|
-
onChange={e => setCustomModel(e.target.value)}
|
|
288
|
-
placeholder="e.g. my-custom-model"
|
|
289
|
-
className={inputCls}
|
|
290
|
-
/>
|
|
291
|
-
) : (
|
|
292
|
-
<select
|
|
293
|
-
value={model}
|
|
294
|
-
onChange={e => setModel(e.target.value)}
|
|
295
|
-
className={selectCls}
|
|
296
|
-
>
|
|
297
|
-
{providerModels[provider].map(m => (
|
|
298
|
-
<option key={m} value={m}>{m}</option>
|
|
299
|
-
))}
|
|
300
|
-
</select>
|
|
301
|
-
)}
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
{/* Actions */}
|
|
305
|
-
<div className="py-4 flex items-center gap-2 flex-wrap">
|
|
306
|
-
<button
|
|
307
|
-
onClick={handleTestConnection}
|
|
308
|
-
disabled={testingConn}
|
|
309
|
-
className={ghostBtn + ' disabled:opacity-40'}
|
|
310
|
-
>
|
|
311
|
-
{testingConn ? 'Testing…' : 'Test connection'}
|
|
312
|
-
</button>
|
|
313
|
-
<button
|
|
314
|
-
onClick={handleSaveProvider}
|
|
315
|
-
disabled={savingProvider}
|
|
316
|
-
className={primaryBtn}
|
|
317
|
-
>
|
|
318
|
-
{savingProvider ? 'Saving…' : saveProviderDone ? 'Saved ✓' : 'Save changes'}
|
|
319
|
-
</button>
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
|
|
324
|
-
{/* ── SECTION 3: Per-User Agent Behaviour ────────────────────────────── */}
|
|
325
|
-
<div className={`rounded-xl border border-border bg-card overflow-hidden transition-opacity ${dimCls}`} aria-disabled={!enabled}>
|
|
326
|
-
{/* Header */}
|
|
327
|
-
<div className="flex items-center gap-3 px-5 py-4 border-b border-border">
|
|
328
|
-
<div className="w-9 h-9 rounded-xl bg-purple-500/10 flex items-center justify-center">
|
|
329
|
-
<Users className="w-4 h-4 text-purple-500" />
|
|
330
|
-
</div>
|
|
331
|
-
<div>
|
|
332
|
-
<h3 className="text-sm font-semibold text-foreground">Per-user agent behaviour</h3>
|
|
333
|
-
<p className="text-xs text-muted-foreground">Fine-tune how the agent behaves for this workspace</p>
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<div className="divide-y divide-border px-5">
|
|
338
|
-
{/* History retention */}
|
|
339
|
-
<div className="flex items-center justify-between py-3 gap-4">
|
|
340
|
-
<div>
|
|
341
|
-
<p className="text-sm font-medium text-foreground">Chat history retention</p>
|
|
342
|
-
<p className="text-xs text-muted-foreground">How long to keep conversation history</p>
|
|
343
|
-
</div>
|
|
344
|
-
<select
|
|
345
|
-
value={historyRetention}
|
|
346
|
-
onChange={e => setHistoryRetention(Number(e.target.value))}
|
|
347
|
-
className="h-9 px-3 rounded-lg border border-border bg-background text-sm text-foreground outline-none focus:ring-1 focus:ring-ring cursor-pointer"
|
|
348
|
-
>
|
|
349
|
-
<option value={30}>30 days</option>
|
|
350
|
-
<option value={90}>90 days</option>
|
|
351
|
-
<option value={180}>180 days</option>
|
|
352
|
-
<option value={0}>Forever</option>
|
|
353
|
-
</select>
|
|
354
|
-
</div>
|
|
355
|
-
|
|
356
|
-
{/* System prompt */}
|
|
357
|
-
<div className="py-3 space-y-2">
|
|
358
|
-
<div className="flex items-center justify-between">
|
|
359
|
-
<div>
|
|
360
|
-
<p className="text-sm font-medium text-foreground">System prompt</p>
|
|
361
|
-
<p className="text-xs text-muted-foreground">Instructions sent to the agent at the start of every conversation</p>
|
|
362
|
-
</div>
|
|
363
|
-
<button
|
|
364
|
-
onClick={() => { setPromptDraft(systemPrompt); setEditingPrompt(true); }}
|
|
365
|
-
className="text-xs font-semibold text-primary hover:underline whitespace-nowrap"
|
|
366
|
-
>
|
|
367
|
-
Edit prompt →
|
|
368
|
-
</button>
|
|
369
|
-
</div>
|
|
370
|
-
<div className="p-3 rounded-lg bg-muted/30 border border-border">
|
|
371
|
-
<p className="text-xs text-muted-foreground font-mono leading-relaxed line-clamp-2">{systemPrompt}</p>
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
{/* Max tokens */}
|
|
376
|
-
<div className="py-3 space-y-1.5">
|
|
377
|
-
<div className="flex items-center justify-between">
|
|
378
|
-
<div>
|
|
379
|
-
<p className="text-sm font-medium text-foreground">Max tokens per turn</p>
|
|
380
|
-
</div>
|
|
381
|
-
<select
|
|
382
|
-
value={maxTokens}
|
|
383
|
-
onChange={e => setMaxTokens(Number(e.target.value))}
|
|
384
|
-
className="h-9 px-3 rounded-lg border border-border bg-background text-sm text-foreground outline-none focus:ring-1 focus:ring-ring cursor-pointer"
|
|
385
|
-
>
|
|
386
|
-
<option value={1000}>1,000</option>
|
|
387
|
-
<option value={4000}>4,000</option>
|
|
388
|
-
<option value={8000}>8,000</option>
|
|
389
|
-
<option value={16000}>16,000</option>
|
|
390
|
-
</select>
|
|
391
|
-
</div>
|
|
392
|
-
<p className="text-xs text-muted-foreground">{tokenCosts[maxTokens]}</p>
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
{/* Can create assignments */}
|
|
396
|
-
<div className="flex items-center justify-between py-3">
|
|
397
|
-
<div>
|
|
398
|
-
<p className="text-sm font-medium text-foreground">Allow agent to create assignments</p>
|
|
399
|
-
<p className="text-xs text-muted-foreground">Agent can assign tasks to users and actors</p>
|
|
400
|
-
</div>
|
|
401
|
-
<Switch
|
|
402
|
-
checked={canCreateAssignments}
|
|
403
|
-
onCheckedChange={setCanCreateAssignments}
|
|
404
|
-
aria-label="Allow agent to create assignments"
|
|
405
|
-
/>
|
|
406
|
-
</div>
|
|
407
|
-
|
|
408
|
-
{/* Can log activities */}
|
|
409
|
-
<div className="flex items-center justify-between py-3">
|
|
410
|
-
<div>
|
|
411
|
-
<p className="text-sm font-medium text-foreground">Allow agent to log activities</p>
|
|
412
|
-
<p className="text-xs text-muted-foreground">Agent can record calls, notes, and emails</p>
|
|
413
|
-
</div>
|
|
414
|
-
<Switch
|
|
415
|
-
checked={canLogActivities}
|
|
416
|
-
onCheckedChange={setCanLogActivities}
|
|
417
|
-
aria-label="Allow agent to log activities"
|
|
418
|
-
/>
|
|
419
|
-
</div>
|
|
420
|
-
|
|
421
|
-
{/* Can write objects */}
|
|
422
|
-
<div className="py-3 space-y-2">
|
|
423
|
-
<div className="flex items-center justify-between">
|
|
424
|
-
<div>
|
|
425
|
-
<p className="text-sm font-medium text-foreground">Allow agent to write CRM objects</p>
|
|
426
|
-
<p className="text-xs text-muted-foreground">Agent can create and edit contacts, accounts, and opportunities</p>
|
|
427
|
-
</div>
|
|
428
|
-
<Switch
|
|
429
|
-
checked={canWriteObjects}
|
|
430
|
-
onCheckedChange={setCanWriteObjects}
|
|
431
|
-
aria-label="Allow agent to write CRM objects"
|
|
432
|
-
/>
|
|
433
|
-
</div>
|
|
434
|
-
{canWriteObjects && (
|
|
435
|
-
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
|
|
436
|
-
<TriangleAlert className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
|
437
|
-
<p className="text-xs text-amber-700 dark:text-amber-400">
|
|
438
|
-
Granting write access means the agent can modify CRM records autonomously. Review agent actions regularly.
|
|
439
|
-
</p>
|
|
440
|
-
</div>
|
|
441
|
-
)}
|
|
442
|
-
</div>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
|
|
446
|
-
{/* ── SECTION 4: Danger Zone ─────────────────────────────────────────── */}
|
|
447
|
-
<div className="rounded-xl border border-red-200 dark:border-red-900/50 bg-card overflow-hidden">
|
|
448
|
-
{/* Header */}
|
|
449
|
-
<div className="flex items-center gap-3 px-5 py-4 border-b border-red-200 dark:border-red-900/50">
|
|
450
|
-
<div className="w-9 h-9 rounded-xl bg-red-500/10 flex items-center justify-center">
|
|
451
|
-
<TriangleAlert className="w-4 h-4 text-red-500" />
|
|
452
|
-
</div>
|
|
453
|
-
<div>
|
|
454
|
-
<h3 className="text-sm font-semibold text-red-600 dark:text-red-400">Danger zone</h3>
|
|
455
|
-
<p className="text-xs text-muted-foreground">Irreversible actions — proceed with care</p>
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
|
|
459
|
-
<div className="divide-y divide-red-100 dark:divide-red-900/30 px-5">
|
|
460
|
-
{/* Clear histories */}
|
|
461
|
-
<div className="flex items-center justify-between py-3 gap-4">
|
|
462
|
-
<div>
|
|
463
|
-
<p className="text-sm font-medium text-foreground">Clear all chat histories</p>
|
|
464
|
-
<p className="text-xs text-muted-foreground">Permanently delete all agent conversation history for this workspace</p>
|
|
465
|
-
</div>
|
|
466
|
-
<button onClick={() => setShowClearConfirm(true)} className={dangerBtn + ' whitespace-nowrap'}>
|
|
467
|
-
Clear all histories
|
|
468
|
-
</button>
|
|
469
|
-
</div>
|
|
470
|
-
|
|
471
|
-
</div>
|
|
472
|
-
</div>
|
|
473
|
-
|
|
474
|
-
{/* ── System Prompt Editor Overlay ───────────────────────────────────── */}
|
|
475
|
-
{editingPrompt && (
|
|
476
|
-
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-foreground/20 backdrop-blur-sm">
|
|
477
|
-
<div className="bg-card border border-border rounded-xl p-6 max-w-2xl w-full mx-4 shadow-xl space-y-4">
|
|
478
|
-
<div className="flex items-center justify-between">
|
|
479
|
-
<h3 className="font-semibold text-foreground">Edit system prompt</h3>
|
|
480
|
-
<button onClick={() => setEditingPrompt(false)} className="text-muted-foreground hover:text-foreground transition-colors">
|
|
481
|
-
<X className="w-4 h-4" />
|
|
482
|
-
</button>
|
|
483
|
-
</div>
|
|
484
|
-
<textarea
|
|
485
|
-
ref={textareaRef}
|
|
486
|
-
value={promptDraft}
|
|
487
|
-
onChange={e => setPromptDraft(e.target.value)}
|
|
488
|
-
rows={10}
|
|
489
|
-
className="w-full px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring font-mono resize-none"
|
|
490
|
-
/>
|
|
491
|
-
<div className="flex gap-2 justify-end">
|
|
492
|
-
<button
|
|
493
|
-
onClick={() => setEditingPrompt(false)}
|
|
494
|
-
className={ghostBtn}
|
|
495
|
-
>
|
|
496
|
-
Cancel
|
|
497
|
-
</button>
|
|
498
|
-
<button
|
|
499
|
-
onClick={() => { setSystemPrompt(promptDraft); setEditingPrompt(false); toast({ title: 'System prompt saved' }); }}
|
|
500
|
-
className={primaryBtn}
|
|
501
|
-
>
|
|
502
|
-
Save prompt
|
|
503
|
-
</button>
|
|
504
|
-
</div>
|
|
505
|
-
</div>
|
|
506
|
-
</div>
|
|
507
|
-
)}
|
|
508
|
-
|
|
509
|
-
{/* ── Clear Confirm Modal ─────────────────────────────────────────────── */}
|
|
510
|
-
{showClearConfirm && (
|
|
511
|
-
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-foreground/20 backdrop-blur-sm">
|
|
512
|
-
<div className="bg-card border border-border rounded-xl p-6 max-w-sm w-full mx-4 shadow-xl space-y-4">
|
|
513
|
-
<h3 className="font-semibold text-foreground">Clear all chat histories?</h3>
|
|
514
|
-
<p className="text-sm text-muted-foreground">
|
|
515
|
-
This is irreversible. Type <code className="font-mono bg-muted px-1 rounded">CLEAR</code> to confirm.
|
|
516
|
-
</p>
|
|
517
|
-
<input
|
|
518
|
-
value={clearConfirmText}
|
|
519
|
-
onChange={e => setClearConfirmText(e.target.value)}
|
|
520
|
-
placeholder="CLEAR"
|
|
521
|
-
className="w-full h-9 px-3 rounded-lg border border-border bg-background text-sm font-mono outline-none focus:ring-1 focus:ring-ring"
|
|
522
|
-
/>
|
|
523
|
-
<div className="flex gap-2 justify-end">
|
|
524
|
-
<button
|
|
525
|
-
onClick={() => { setShowClearConfirm(false); setClearConfirmText(''); }}
|
|
526
|
-
className={ghostBtn}
|
|
527
|
-
>
|
|
528
|
-
Cancel
|
|
529
|
-
</button>
|
|
530
|
-
<button
|
|
531
|
-
disabled={clearConfirmText !== 'CLEAR'}
|
|
532
|
-
onClick={handleClearAll}
|
|
533
|
-
className={dangerBtn + ' disabled:opacity-40'}
|
|
534
|
-
>
|
|
535
|
-
Clear histories
|
|
536
|
-
</button>
|
|
537
|
-
</div>
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
)}
|
|
541
|
-
|
|
542
|
-
</div>
|
|
543
|
-
);
|
|
544
|
-
}
|