@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.
Files changed (121) hide show
  1. package/dist/assets/index-CskfWp8E.js +560 -0
  2. package/dist/assets/index-D763l57m.css +1 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +4 -1
  5. package/postcss.config.js +0 -6
  6. package/src/App.tsx +0 -158
  7. package/src/api/client.ts +0 -82
  8. package/src/api/hooks.ts +0 -689
  9. package/src/components/CustomFields.tsx +0 -240
  10. package/src/components/NavLink.tsx +0 -28
  11. package/src/components/crm/AIFab.tsx +0 -37
  12. package/src/components/crm/AccountDrawer.tsx +0 -372
  13. package/src/components/crm/ActivityTimeline.tsx +0 -115
  14. package/src/components/crm/AssignmentDrawer.tsx +0 -396
  15. package/src/components/crm/BriefingPanel.tsx +0 -217
  16. package/src/components/crm/CommandPalette.tsx +0 -254
  17. package/src/components/crm/ContactAvatar.tsx +0 -49
  18. package/src/components/crm/ContactDrawer.tsx +0 -438
  19. package/src/components/crm/ContextPanel.tsx +0 -200
  20. package/src/components/crm/CrmWidgets.tsx +0 -417
  21. package/src/components/crm/DrawerShell.tsx +0 -77
  22. package/src/components/crm/ListToolbar.tsx +0 -252
  23. package/src/components/crm/OpportunityDrawer.tsx +0 -372
  24. package/src/components/crm/PaginationBar.tsx +0 -111
  25. package/src/components/crm/QuickAddDrawer.tsx +0 -652
  26. package/src/components/crm/ShortcutsOverlay.tsx +0 -65
  27. package/src/components/crm/UseCaseDrawer.tsx +0 -454
  28. package/src/components/layout/MobileNav.tsx +0 -49
  29. package/src/components/layout/Sidebar.tsx +0 -157
  30. package/src/components/layout/TopBar.tsx +0 -54
  31. package/src/components/settings/ActorsSettings.tsx +0 -1190
  32. package/src/components/ui/accordion.tsx +0 -52
  33. package/src/components/ui/alert-dialog.tsx +0 -104
  34. package/src/components/ui/alert.tsx +0 -43
  35. package/src/components/ui/aspect-ratio.tsx +0 -5
  36. package/src/components/ui/avatar.tsx +0 -38
  37. package/src/components/ui/badge.tsx +0 -29
  38. package/src/components/ui/breadcrumb.tsx +0 -90
  39. package/src/components/ui/button.tsx +0 -47
  40. package/src/components/ui/calendar.tsx +0 -54
  41. package/src/components/ui/card.tsx +0 -43
  42. package/src/components/ui/carousel.tsx +0 -224
  43. package/src/components/ui/chart.tsx +0 -303
  44. package/src/components/ui/checkbox.tsx +0 -26
  45. package/src/components/ui/collapsible.tsx +0 -9
  46. package/src/components/ui/command.tsx +0 -132
  47. package/src/components/ui/context-menu.tsx +0 -178
  48. package/src/components/ui/date-picker.tsx +0 -313
  49. package/src/components/ui/dialog.tsx +0 -95
  50. package/src/components/ui/drawer.tsx +0 -87
  51. package/src/components/ui/dropdown-menu.tsx +0 -179
  52. package/src/components/ui/form.tsx +0 -129
  53. package/src/components/ui/hover-card.tsx +0 -27
  54. package/src/components/ui/input-otp.tsx +0 -61
  55. package/src/components/ui/input.tsx +0 -22
  56. package/src/components/ui/label.tsx +0 -17
  57. package/src/components/ui/menubar.tsx +0 -207
  58. package/src/components/ui/navigation-menu.tsx +0 -120
  59. package/src/components/ui/pagination.tsx +0 -81
  60. package/src/components/ui/popover.tsx +0 -29
  61. package/src/components/ui/progress.tsx +0 -23
  62. package/src/components/ui/radio-group.tsx +0 -36
  63. package/src/components/ui/resizable.tsx +0 -37
  64. package/src/components/ui/scroll-area.tsx +0 -38
  65. package/src/components/ui/select.tsx +0 -143
  66. package/src/components/ui/separator.tsx +0 -20
  67. package/src/components/ui/sheet.tsx +0 -107
  68. package/src/components/ui/sidebar.tsx +0 -637
  69. package/src/components/ui/skeleton.tsx +0 -7
  70. package/src/components/ui/slider.tsx +0 -23
  71. package/src/components/ui/sonner.tsx +0 -24
  72. package/src/components/ui/switch.tsx +0 -27
  73. package/src/components/ui/table.tsx +0 -72
  74. package/src/components/ui/tabs.tsx +0 -53
  75. package/src/components/ui/textarea.tsx +0 -21
  76. package/src/components/ui/toast.tsx +0 -111
  77. package/src/components/ui/toaster.tsx +0 -24
  78. package/src/components/ui/toggle-group.tsx +0 -49
  79. package/src/components/ui/toggle.tsx +0 -37
  80. package/src/components/ui/tooltip.tsx +0 -28
  81. package/src/components/ui/use-toast.ts +0 -1
  82. package/src/components/ui/utils.ts +0 -9
  83. package/src/contexts/AgentSettingsContext.tsx +0 -24
  84. package/src/hooks/use-mobile.tsx +0 -19
  85. package/src/hooks/use-toast.ts +0 -186
  86. package/src/hooks/useKeyboardShortcuts.ts +0 -95
  87. package/src/hooks/useTheme.ts +0 -24
  88. package/src/index.css +0 -245
  89. package/src/lib/entityColors.ts +0 -18
  90. package/src/lib/stageConfig.ts +0 -32
  91. package/src/lib/utils.ts +0 -6
  92. package/src/main.tsx +0 -25
  93. package/src/pages/Accounts.tsx +0 -205
  94. package/src/pages/Activities.tsx +0 -251
  95. package/src/pages/Agent.tsx +0 -237
  96. package/src/pages/AgentSettings.tsx +0 -544
  97. package/src/pages/Assignments.tsx +0 -750
  98. package/src/pages/Contacts.tsx +0 -200
  99. package/src/pages/Dashboard.tsx +0 -143
  100. package/src/pages/Inbox.tsx +0 -615
  101. package/src/pages/NotFound.tsx +0 -24
  102. package/src/pages/Opportunities.tsx +0 -386
  103. package/src/pages/SearchResults.tsx +0 -49
  104. package/src/pages/Settings.tsx +0 -1884
  105. package/src/pages/UseCases.tsx +0 -396
  106. package/src/pages/auth/Login.tsx +0 -261
  107. package/src/pages/hitl/HITL.tsx +0 -101
  108. package/src/store/appStore.ts +0 -103
  109. package/src/vite-env.d.ts +0 -14
  110. package/tailwind.config.js +0 -121
  111. package/tsconfig.json +0 -24
  112. package/vite.config.ts +0 -27
  113. /package/{public → dist}/android-chrome-192x192.png +0 -0
  114. /package/{public → dist}/android-chrome-512x512.png +0 -0
  115. /package/{public → dist}/apple-touch-icon.png +0 -0
  116. /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
  117. /package/{public → dist}/favicon-16x16.png +0 -0
  118. /package/{public → dist}/favicon-32x32.png +0 -0
  119. /package/{public → dist}/favicon.ico +0 -0
  120. /package/{public → dist}/favicon.svg +0 -0
  121. /package/{public → dist}/site.webmanifest +0 -0
@@ -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
- }
@@ -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
- }