@crmy/web 0.5.5 → 0.5.6
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
package/src/pages/Activities.tsx
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 CRMy Contributors
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
import { useState, useMemo, useEffect } from 'react';
|
|
5
|
-
import { TopBar } from '@/components/layout/TopBar';
|
|
6
|
-
import { useActivities } from '@/api/hooks';
|
|
7
|
-
import { ActivityFeed } from '@/components/crm/CrmWidgets';
|
|
8
|
-
import { PaginationBar } from '@/components/crm/PaginationBar';
|
|
9
|
-
import { useAppStore } from '@/store/appStore';
|
|
10
|
-
import { ListToolbar, type FilterConfig, type SortOption } from '@/components/crm/ListToolbar';
|
|
11
|
-
import { DatePicker } from '@/components/ui/date-picker';
|
|
12
|
-
|
|
13
|
-
const filterConfigs: FilterConfig[] = [
|
|
14
|
-
{
|
|
15
|
-
key: 'type', label: 'Type',
|
|
16
|
-
options: [
|
|
17
|
-
{ value: 'call', label: 'Call' },
|
|
18
|
-
{ value: 'email', label: 'Email' },
|
|
19
|
-
{ value: 'meeting', label: 'Meeting' },
|
|
20
|
-
{ value: 'note', label: 'Note' },
|
|
21
|
-
{ value: 'task', label: 'Task' },
|
|
22
|
-
{ value: 'demo', label: 'Demo' },
|
|
23
|
-
{ value: 'proposal', label: 'Proposal' },
|
|
24
|
-
{ value: 'research', label: 'Research' },
|
|
25
|
-
{ value: 'handoff', label: 'Handoff' },
|
|
26
|
-
{ value: 'status_update', label: 'Status Update' },
|
|
27
|
-
],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
key: 'subject_type', label: 'Subject',
|
|
31
|
-
options: [
|
|
32
|
-
{ value: 'contact', label: 'Contact' },
|
|
33
|
-
{ value: 'account', label: 'Account' },
|
|
34
|
-
{ value: 'opportunity', label: 'Opportunity' },
|
|
35
|
-
{ value: 'use_case', label: 'Use Case' },
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
key: 'outcome', label: 'Outcome',
|
|
40
|
-
options: [
|
|
41
|
-
{ value: 'connected', label: 'Connected' },
|
|
42
|
-
{ value: 'voicemail', label: 'Voicemail' },
|
|
43
|
-
{ value: 'positive', label: 'Positive' },
|
|
44
|
-
{ value: 'negative', label: 'Negative' },
|
|
45
|
-
{ value: 'neutral', label: 'Neutral' },
|
|
46
|
-
{ value: 'no_show', label: 'No Show' },
|
|
47
|
-
{ value: 'follow_up_needed', label: 'Follow-up Needed' },
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const sortOptions: SortOption[] = [
|
|
53
|
-
{ key: 'occurred_at', label: 'When' },
|
|
54
|
-
{ key: 'created_at', label: 'Logged' },
|
|
55
|
-
{ key: 'type', label: 'Type' },
|
|
56
|
-
{ key: 'outcome', label: 'Outcome' },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
type TimeRangePreset = 'today' | 'this_week' | 'this_month' | 'this_quarter' | 'custom';
|
|
60
|
-
|
|
61
|
-
const TIME_RANGE_OPTIONS: { value: TimeRangePreset; label: string }[] = [
|
|
62
|
-
{ value: 'today', label: 'Today' },
|
|
63
|
-
{ value: 'this_week', label: 'This Week' },
|
|
64
|
-
{ value: 'this_month', label: 'This Month' },
|
|
65
|
-
{ value: 'this_quarter', label: 'This Quarter' },
|
|
66
|
-
{ value: 'custom', label: 'Custom' },
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
function getPresetDates(preset: TimeRangePreset): { start: Date; end: Date } {
|
|
70
|
-
const now = new Date();
|
|
71
|
-
const end = new Date(now);
|
|
72
|
-
end.setHours(23, 59, 59, 999);
|
|
73
|
-
|
|
74
|
-
if (preset === 'today') {
|
|
75
|
-
const start = new Date(now);
|
|
76
|
-
start.setHours(0, 0, 0, 0);
|
|
77
|
-
return { start, end };
|
|
78
|
-
}
|
|
79
|
-
if (preset === 'this_week') {
|
|
80
|
-
const start = new Date(now);
|
|
81
|
-
const day = start.getDay(); // 0=Sun, 1=Mon...
|
|
82
|
-
const diff = day === 0 ? -6 : 1 - day; // adjust to Monday
|
|
83
|
-
start.setDate(start.getDate() + diff);
|
|
84
|
-
start.setHours(0, 0, 0, 0);
|
|
85
|
-
return { start, end };
|
|
86
|
-
}
|
|
87
|
-
if (preset === 'this_month') {
|
|
88
|
-
const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
89
|
-
return { start, end };
|
|
90
|
-
}
|
|
91
|
-
if (preset === 'this_quarter') {
|
|
92
|
-
const quarter = Math.floor(now.getMonth() / 3);
|
|
93
|
-
const start = new Date(now.getFullYear(), quarter * 3, 1, 0, 0, 0, 0);
|
|
94
|
-
return { start, end };
|
|
95
|
-
}
|
|
96
|
-
// custom — caller provides dates
|
|
97
|
-
return { start: new Date(0), end };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
-
type Activity = any;
|
|
102
|
-
|
|
103
|
-
export default function Activities() {
|
|
104
|
-
const { openQuickAdd } = useAppStore();
|
|
105
|
-
const [search, setSearch] = useState('');
|
|
106
|
-
const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>({});
|
|
107
|
-
const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(null);
|
|
108
|
-
const [timeRange, setTimeRange] = useState<TimeRangePreset>('this_week');
|
|
109
|
-
const [customFrom, setCustomFrom] = useState('');
|
|
110
|
-
const [customTo, setCustomTo] = useState('');
|
|
111
|
-
const [page, setPage] = useState(1);
|
|
112
|
-
const PAGE_SIZE = 50;
|
|
113
|
-
|
|
114
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
-
const { data, isLoading } = useActivities({ limit: 200 }) as any;
|
|
116
|
-
const allActivities: Activity[] = data?.data ?? [];
|
|
117
|
-
|
|
118
|
-
const handleFilterChange = (key: string, values: string[]) => {
|
|
119
|
-
setActiveFilters(prev => {
|
|
120
|
-
const next = { ...prev };
|
|
121
|
-
if (values.length === 0) delete next[key]; else next[key] = values;
|
|
122
|
-
return next;
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const handleSortChange = (key: string) => {
|
|
127
|
-
setSort(prev => prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'desc' });
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const filtered = useMemo(() => {
|
|
131
|
-
let result = [...allActivities];
|
|
132
|
-
|
|
133
|
-
// Date range filtering — use occurred_at (falling back to created_at)
|
|
134
|
-
let start: Date;
|
|
135
|
-
let end: Date;
|
|
136
|
-
if (timeRange === 'custom') {
|
|
137
|
-
start = customFrom ? new Date(customFrom + 'T00:00:00') : new Date(0);
|
|
138
|
-
end = customTo ? new Date(customTo + 'T23:59:59') : new Date(8640000000000000);
|
|
139
|
-
} else {
|
|
140
|
-
({ start, end } = getPresetDates(timeRange));
|
|
141
|
-
}
|
|
142
|
-
result = result.filter(a => {
|
|
143
|
-
const d = new Date(a.occurred_at ?? a.created_at);
|
|
144
|
-
return d >= start && d <= end;
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (search) {
|
|
148
|
-
const q = search.toLowerCase();
|
|
149
|
-
result = result.filter(a => {
|
|
150
|
-
const desc = ((a.description ?? a.body ?? a.subject ?? '') as string).toLowerCase();
|
|
151
|
-
const name = ((a.contact_name ?? '') as string).toLowerCase();
|
|
152
|
-
const outcome = ((a.outcome ?? '') as string).toLowerCase();
|
|
153
|
-
const subjectType = ((a.subject_type ?? '') as string).replace(/_/g, ' ').toLowerCase();
|
|
154
|
-
return desc.includes(q) || name.includes(q) || outcome.includes(q) || subjectType.includes(q);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
if (activeFilters.type?.length) {
|
|
158
|
-
result = result.filter(a => activeFilters.type.includes(a.type as string));
|
|
159
|
-
}
|
|
160
|
-
if (activeFilters.subject_type?.length) {
|
|
161
|
-
result = result.filter(a => activeFilters.subject_type.includes(a.subject_type as string));
|
|
162
|
-
}
|
|
163
|
-
if (activeFilters.outcome?.length) {
|
|
164
|
-
result = result.filter(a => activeFilters.outcome.includes(a.outcome as string));
|
|
165
|
-
}
|
|
166
|
-
if (sort) {
|
|
167
|
-
result.sort((a, b) => {
|
|
168
|
-
let aVal: string;
|
|
169
|
-
let bVal: string;
|
|
170
|
-
if (sort.key === 'occurred_at') {
|
|
171
|
-
aVal = a.occurred_at ?? a.created_at ?? '';
|
|
172
|
-
bVal = b.occurred_at ?? b.created_at ?? '';
|
|
173
|
-
} else {
|
|
174
|
-
aVal = (a[sort.key] ?? '') as string;
|
|
175
|
-
bVal = (b[sort.key] ?? '') as string;
|
|
176
|
-
}
|
|
177
|
-
return sort.dir === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return result;
|
|
181
|
-
}, [allActivities, search, activeFilters, sort, timeRange, customFrom, customTo]);
|
|
182
|
-
|
|
183
|
-
useEffect(() => { setPage(1); }, [search, activeFilters, sort, timeRange, customFrom, customTo]);
|
|
184
|
-
const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
185
|
-
|
|
186
|
-
return (
|
|
187
|
-
<div className="flex flex-col h-full">
|
|
188
|
-
<TopBar title="Activities" />
|
|
189
|
-
|
|
190
|
-
{/* Time range selector */}
|
|
191
|
-
<div className="px-4 md:px-6 pt-3 pb-1 flex flex-wrap items-center gap-2">
|
|
192
|
-
<div className="inline-flex rounded-xl border border-border bg-muted/40 p-0.5 gap-0.5">
|
|
193
|
-
{TIME_RANGE_OPTIONS.map(opt => (
|
|
194
|
-
<button
|
|
195
|
-
key={opt.value}
|
|
196
|
-
onClick={() => setTimeRange(opt.value)}
|
|
197
|
-
className={[
|
|
198
|
-
'px-3 py-1.5 text-xs font-medium rounded-lg transition-all',
|
|
199
|
-
timeRange === opt.value
|
|
200
|
-
? 'bg-background text-foreground shadow-sm'
|
|
201
|
-
: 'text-muted-foreground hover:text-foreground',
|
|
202
|
-
].join(' ')}
|
|
203
|
-
>
|
|
204
|
-
{opt.label}
|
|
205
|
-
</button>
|
|
206
|
-
))}
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
{timeRange === 'custom' && (
|
|
210
|
-
<div className="flex items-center gap-2">
|
|
211
|
-
<DatePicker
|
|
212
|
-
value={customFrom}
|
|
213
|
-
onChange={setCustomFrom}
|
|
214
|
-
size="sm"
|
|
215
|
-
placeholder="From"
|
|
216
|
-
className="w-36"
|
|
217
|
-
/>
|
|
218
|
-
<span className="text-xs text-muted-foreground">to</span>
|
|
219
|
-
<DatePicker
|
|
220
|
-
value={customTo}
|
|
221
|
-
onChange={setCustomTo}
|
|
222
|
-
size="sm"
|
|
223
|
-
placeholder="To"
|
|
224
|
-
className="w-36"
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
<ListToolbar
|
|
231
|
-
searchValue={search} onSearchChange={setSearch} searchPlaceholder="Search activities..."
|
|
232
|
-
filters={filterConfigs} activeFilters={activeFilters} onFilterChange={handleFilterChange}
|
|
233
|
-
onClearFilters={() => setActiveFilters({})} sortOptions={sortOptions} currentSort={sort}
|
|
234
|
-
onSortChange={handleSortChange} onAdd={() => openQuickAdd('activity')} addLabel="Log Activity" entityType="activities"
|
|
235
|
-
/>
|
|
236
|
-
|
|
237
|
-
<div className="flex-1 overflow-y-auto px-4 md:px-6 pb-24 md:pb-6">
|
|
238
|
-
{isLoading ? (
|
|
239
|
-
<div className="space-y-3 pt-2">
|
|
240
|
-
{[...Array(6)].map((_, i) => <div key={i} className="h-12 bg-muted/50 rounded-xl animate-pulse" />)}
|
|
241
|
-
</div>
|
|
242
|
-
) : (
|
|
243
|
-
<div className="bg-card border border-border rounded-2xl p-4 shadow-sm">
|
|
244
|
-
<ActivityFeed activities={paginated} />
|
|
245
|
-
<PaginationBar page={page} pageSize={PAGE_SIZE} total={filtered.length} onPageChange={setPage} />
|
|
246
|
-
</div>
|
|
247
|
-
)}
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
);
|
|
251
|
-
}
|
package/src/pages/Agent.tsx
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { TopBar } from '@/components/layout/TopBar';
|
|
3
|
-
import { AgentStatusDot } from '@/components/crm/CrmWidgets';
|
|
4
|
-
import { useAppStore, type AIContextEntity } from '@/store/appStore';
|
|
5
|
-
import { Send, Sparkles, CheckCircle, Bot, X, User, Briefcase, Building, Layers, Clock } from 'lucide-react';
|
|
6
|
-
import { motion } from 'framer-motion';
|
|
7
|
-
|
|
8
|
-
type Message = { role: 'agent' | 'user'; content: string };
|
|
9
|
-
|
|
10
|
-
const defaultMessages: Message[] = [
|
|
11
|
-
{ role: 'agent', content: "Good morning! I've reviewed your pipeline overnight. Here's what needs attention:\n\n1. **Sarah Chen's deal** — appraisal should arrive today. I'll draft a follow-up email once it's in.\n2. **Hannah Williams** — her Brooklyn viewings are stale (16 days). I suggest scheduling new showings.\n3. **Sacramento listing** for Maria Santos has been in lead stage for 22 days. Consider a nurture sequence.\n\nWant me to take action on any of these?" },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const typeIcons: Record<string, typeof User> = {
|
|
15
|
-
contact: User,
|
|
16
|
-
opportunity: Briefcase,
|
|
17
|
-
'use-case': Layers,
|
|
18
|
-
account: Building,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const typeLabels: Record<string, string> = {
|
|
22
|
-
contact: 'Contact',
|
|
23
|
-
opportunity: 'Opportunity',
|
|
24
|
-
'use-case': 'Use Case',
|
|
25
|
-
account: 'Account',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
interface Session {
|
|
29
|
-
id: string;
|
|
30
|
-
label: string;
|
|
31
|
-
messages: Message[];
|
|
32
|
-
context: AIContextEntity | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const mockSessions: Session[] = [
|
|
36
|
-
{ id: '1', label: 'Follow-up for Sarah Chen', messages: [
|
|
37
|
-
{ role: 'agent', content: "I drafted a follow-up email for **Sarah Chen** regarding the appraisal results. Ready for your review." },
|
|
38
|
-
{ role: 'user', content: "Looks good, send it." },
|
|
39
|
-
{ role: 'agent', content: "✅ Email sent to Sarah Chen." },
|
|
40
|
-
], context: { type: 'opportunity', id: 'd1', name: "Sarah Chen's Opportunity", detail: '$850K' } },
|
|
41
|
-
{ id: '2', label: 'Nurture sequence for Maria Santos', messages: [
|
|
42
|
-
{ role: 'agent', content: "I created a 4-email nurture sequence for **Maria Santos**. Starting with a home value assessment offer." },
|
|
43
|
-
{ role: 'user', content: "Add a market trends email as step 2." },
|
|
44
|
-
{ role: 'agent', content: "✅ Updated. The sequence now includes market trends as step 2." },
|
|
45
|
-
], context: { type: 'contact', id: 'c3', name: 'Maria Santos', detail: 'Sacramento' } },
|
|
46
|
-
{ id: '3', label: 'Pipeline review — Q1', messages: [
|
|
47
|
-
{ role: 'agent', content: "Here's your Q1 pipeline summary:\n\n• **Weighted pipeline**: $1.2M\n• **Best case**: $2.1M\n• **3 deals** stale >14 days\n\nWant me to flag the stale deals?" },
|
|
48
|
-
], context: null },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
export default function Agent() {
|
|
52
|
-
const [messages, setMessages] = useState<Message[]>(defaultMessages);
|
|
53
|
-
const [input, setInput] = useState('');
|
|
54
|
-
const [entityContext, setEntityContext] = useState<AIContextEntity | null>(null);
|
|
55
|
-
const { aiContext } = useAppStore();
|
|
56
|
-
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (aiContext) {
|
|
59
|
-
setEntityContext(aiContext);
|
|
60
|
-
setInput(`Update ${aiContext.name}: `);
|
|
61
|
-
setMessages([{
|
|
62
|
-
role: 'agent',
|
|
63
|
-
content: `I'm ready to help with **${aiContext.name}**${aiContext.detail ? ` (${aiContext.detail})` : ''}. What would you like to update?`
|
|
64
|
-
}]);
|
|
65
|
-
useAppStore.setState({ aiContext: null });
|
|
66
|
-
}
|
|
67
|
-
}, [aiContext]);
|
|
68
|
-
|
|
69
|
-
const sendMessage = () => {
|
|
70
|
-
if (!input.trim()) return;
|
|
71
|
-
setMessages([...messages, { role: 'user', content: input }]);
|
|
72
|
-
setInput('');
|
|
73
|
-
setTimeout(() => {
|
|
74
|
-
setMessages((prev) => [...prev, {
|
|
75
|
-
role: 'agent',
|
|
76
|
-
content: "I'm processing your request. In a real implementation, this would connect to an AI model to take actions on your CRM data. 🚀"
|
|
77
|
-
}]);
|
|
78
|
-
}, 1000);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const suggestions = entityContext
|
|
82
|
-
? [`Update ${typeLabels[entityContext.type].toLowerCase()} details`, `Summarize activity`, `Draft follow-up`]
|
|
83
|
-
: ['Summarize pipeline', 'Draft follow-up for Sarah', 'Deals needing attention'];
|
|
84
|
-
|
|
85
|
-
const IconComponent = entityContext ? typeIcons[entityContext.type] : null;
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className="flex flex-col h-full">
|
|
89
|
-
<TopBar title="AI Agent" />
|
|
90
|
-
<div className="flex-1 flex flex-col lg:flex-row overflow-hidden">
|
|
91
|
-
{/* Chat */}
|
|
92
|
-
<div className="flex-1 flex flex-col min-w-0">
|
|
93
|
-
{/* Agent header */}
|
|
94
|
-
<div className="px-4 py-3 border-b border-border bg-gradient-to-r from-primary/5 to-accent/5">
|
|
95
|
-
<div className="flex items-center gap-2">
|
|
96
|
-
<AgentStatusDot />
|
|
97
|
-
<span className="text-sm font-display font-bold text-foreground">AI Agent active</span>
|
|
98
|
-
<span className="text-[10px] text-muted-foreground ml-auto bg-muted px-2 py-0.5 rounded-full">Synced 2m ago</span>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
{/* Context banner */}
|
|
103
|
-
{entityContext && IconComponent && (
|
|
104
|
-
<motion.div
|
|
105
|
-
initial={{ opacity: 0, height: 0 }}
|
|
106
|
-
animate={{ opacity: 1, height: 'auto' }}
|
|
107
|
-
className="px-4 py-2.5 border-b border-border bg-accent/5"
|
|
108
|
-
>
|
|
109
|
-
<div className="flex items-center gap-2.5">
|
|
110
|
-
<div className="w-7 h-7 rounded-lg bg-accent/15 flex items-center justify-center">
|
|
111
|
-
<IconComponent className="w-3.5 h-3.5 text-accent" />
|
|
112
|
-
</div>
|
|
113
|
-
<div className="flex-1 min-w-0">
|
|
114
|
-
<p className="text-xs text-muted-foreground">{typeLabels[entityContext.type]}</p>
|
|
115
|
-
<p className="text-sm font-display font-bold text-foreground truncate">
|
|
116
|
-
{entityContext.name}
|
|
117
|
-
{entityContext.detail && <span className="font-normal text-muted-foreground ml-1.5">· {entityContext.detail}</span>}
|
|
118
|
-
</p>
|
|
119
|
-
</div>
|
|
120
|
-
<button
|
|
121
|
-
onClick={() => setEntityContext(null)}
|
|
122
|
-
className="p-1 rounded-md hover:bg-muted transition-colors"
|
|
123
|
-
>
|
|
124
|
-
<X className="w-3.5 h-3.5 text-muted-foreground" />
|
|
125
|
-
</button>
|
|
126
|
-
</div>
|
|
127
|
-
</motion.div>
|
|
128
|
-
)}
|
|
129
|
-
|
|
130
|
-
{/* Messages */}
|
|
131
|
-
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-24 md:pb-4">
|
|
132
|
-
{messages.map((msg, i) => (
|
|
133
|
-
<motion.div
|
|
134
|
-
key={i}
|
|
135
|
-
initial={{ opacity: 0, y: 8 }}
|
|
136
|
-
animate={{ opacity: 1, y: 0 }}
|
|
137
|
-
transition={{ delay: i * 0.05 }}
|
|
138
|
-
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
139
|
-
>
|
|
140
|
-
{msg.role === 'agent' && (
|
|
141
|
-
<div className="w-8 h-8 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center mr-2 flex-shrink-0 mt-1">
|
|
142
|
-
<Bot className="w-4 h-4 text-primary" />
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
145
|
-
<div
|
|
146
|
-
className={`max-w-[75%] rounded-2xl px-4 py-3 text-sm whitespace-pre-wrap
|
|
147
|
-
${msg.role === 'user'
|
|
148
|
-
? 'bg-gradient-to-br from-primary to-primary/80 text-primary-foreground rounded-br-md'
|
|
149
|
-
: 'bg-card border border-border text-foreground rounded-bl-md shadow-sm'
|
|
150
|
-
}`}
|
|
151
|
-
>
|
|
152
|
-
{msg.content}
|
|
153
|
-
</div>
|
|
154
|
-
</motion.div>
|
|
155
|
-
))}
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{/* Suggestions */}
|
|
159
|
-
<div className="px-4 flex gap-2 flex-wrap">
|
|
160
|
-
{suggestions.map((s) => (
|
|
161
|
-
<button
|
|
162
|
-
key={s}
|
|
163
|
-
onClick={() => setInput(s)}
|
|
164
|
-
className="px-3.5 py-2 rounded-xl text-xs bg-card border border-border text-muted-foreground hover:bg-primary/5 hover:text-primary hover:border-primary/30 transition-all press-scale"
|
|
165
|
-
>
|
|
166
|
-
{s}
|
|
167
|
-
</button>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
{/* Input */}
|
|
172
|
-
<div className="p-4">
|
|
173
|
-
<div className="flex gap-2 items-end bg-card border border-border rounded-2xl p-2 shadow-sm">
|
|
174
|
-
<textarea
|
|
175
|
-
value={input}
|
|
176
|
-
onChange={(e) => setInput(e.target.value)}
|
|
177
|
-
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), sendMessage())}
|
|
178
|
-
placeholder="Ask your AI agent..."
|
|
179
|
-
rows={1}
|
|
180
|
-
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground resize-none outline-none px-2 py-1.5"
|
|
181
|
-
/>
|
|
182
|
-
<button
|
|
183
|
-
onClick={sendMessage}
|
|
184
|
-
disabled={!input.trim()}
|
|
185
|
-
className="p-2.5 rounded-xl bg-gradient-to-br from-primary to-primary/80 text-primary-foreground hover:shadow-md disabled:opacity-40 transition-all press-scale"
|
|
186
|
-
>
|
|
187
|
-
<Send className="w-4 h-4" />
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
{/* Context panel (desktop only) */}
|
|
194
|
-
<div className="hidden lg:flex flex-col w-80 border-l border-border bg-surface">
|
|
195
|
-
<div className="p-5 border-b border-border">
|
|
196
|
-
<h3 className="font-display font-bold text-foreground text-sm mb-3">Connected tools</h3>
|
|
197
|
-
<div className="space-y-2.5">
|
|
198
|
-
{[
|
|
199
|
-
{ name: 'Google Calendar', status: true },
|
|
200
|
-
{ name: 'Email (Gmail)', status: true },
|
|
201
|
-
{ name: 'MCP Server', status: true },
|
|
202
|
-
].map((tool) => (
|
|
203
|
-
<div key={tool.name} className="flex items-center justify-between text-sm">
|
|
204
|
-
<span className="text-foreground">{tool.name}</span>
|
|
205
|
-
{tool.status
|
|
206
|
-
? <CheckCircle className="w-4 h-4 text-success" />
|
|
207
|
-
: <span className="text-[10px] text-muted-foreground bg-muted px-2 py-0.5 rounded-full">Not connected</span>
|
|
208
|
-
}
|
|
209
|
-
</div>
|
|
210
|
-
))}
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<div className="p-5 flex-1 overflow-y-auto">
|
|
215
|
-
<h3 className="font-display font-bold text-foreground text-sm mb-3">Recent sessions</h3>
|
|
216
|
-
<div className="space-y-1.5">
|
|
217
|
-
{mockSessions.map((session) => (
|
|
218
|
-
<button
|
|
219
|
-
key={session.id}
|
|
220
|
-
onClick={() => {
|
|
221
|
-
setMessages(session.messages);
|
|
222
|
-
setEntityContext(session.context);
|
|
223
|
-
setInput('');
|
|
224
|
-
}}
|
|
225
|
-
className="w-full flex items-start gap-2.5 px-3 py-2.5 rounded-xl text-left hover:bg-muted/50 transition-colors group"
|
|
226
|
-
>
|
|
227
|
-
<Clock className="w-3.5 h-3.5 text-muted-foreground mt-0.5 flex-shrink-0" />
|
|
228
|
-
<span className="text-xs text-muted-foreground group-hover:text-foreground transition-colors line-clamp-2">{session.label}</span>
|
|
229
|
-
</button>
|
|
230
|
-
))}
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
);
|
|
237
|
-
}
|