@rahul_vendure/ai-chat-dashboard 0.1.2 → 0.2.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 +14 -0
- package/README.md +2 -1
- package/package.json +7 -5
- package/src/dashboard/AdminChatWidget.tsx +116 -272
- package/src/dashboard/components/AiChatToolbarButton.tsx +308 -0
- package/src/dashboard/components/ChatEmptyState.tsx +61 -0
- package/src/dashboard/components/ChatInput.tsx +76 -0
- package/src/dashboard/components/ChatMessageBubble.tsx +110 -0
- package/src/dashboard/components/EmbedAllButton.tsx +69 -0
- package/src/dashboard/components/OrderCard.tsx +66 -0
- package/src/dashboard/components/ProductCard.tsx +40 -0
- package/src/dashboard/components/SessionSidebar.tsx +119 -0
- package/src/dashboard/hooks/use-admin-chat.ts +138 -0
- package/src/dashboard/hooks/use-session-list.ts +69 -0
- package/src/dashboard/index.tsx +18 -24
- package/src/dashboard/types.ts +46 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ArrowUpRight } from 'lucide-react';
|
|
3
|
+
import type { Order } from '../types';
|
|
4
|
+
|
|
5
|
+
interface OrderCardProps {
|
|
6
|
+
order: Order;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getStateColor(state: string): string {
|
|
10
|
+
switch (state) {
|
|
11
|
+
case 'PaymentSettled':
|
|
12
|
+
case 'Delivered':
|
|
13
|
+
return '#10b981';
|
|
14
|
+
case 'Cancelled':
|
|
15
|
+
return '#ef4444';
|
|
16
|
+
case 'Shipped':
|
|
17
|
+
case 'PartiallyShipped':
|
|
18
|
+
return '#3b82f6';
|
|
19
|
+
default:
|
|
20
|
+
return '#94a3b8';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function OrderCard({ order }: OrderCardProps) {
|
|
25
|
+
const total = `$${(order.total / 100).toFixed(2)}`;
|
|
26
|
+
const date = order.orderPlacedAt
|
|
27
|
+
? new Date(order.orderPlacedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
28
|
+
: '';
|
|
29
|
+
const [hovered, setHovered] = useState(false);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
style={{
|
|
34
|
+
display: 'flex', alignItems: 'center', gap: 16,
|
|
35
|
+
borderRadius: 8, padding: '12px 16px', cursor: 'pointer',
|
|
36
|
+
border: '1px solid var(--border, #e2e8f0)',
|
|
37
|
+
background: hovered ? 'var(--muted, #f8fafc)' : 'var(--background, #fff)',
|
|
38
|
+
transition: 'background 0.15s',
|
|
39
|
+
}}
|
|
40
|
+
onClick={() => window.open(`/dashboard/orders/${order.id}`, '_self')}
|
|
41
|
+
onMouseEnter={() => setHovered(true)}
|
|
42
|
+
onMouseLeave={() => setHovered(false)}
|
|
43
|
+
>
|
|
44
|
+
{/* Status dot */}
|
|
45
|
+
<div style={{ width: 8, height: 8, borderRadius: '50%', flexShrink: 0, background: getStateColor(order.state) }} />
|
|
46
|
+
|
|
47
|
+
{/* Info */}
|
|
48
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
49
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
50
|
+
<span style={{ fontFamily: 'monospace', fontSize: 14, fontWeight: 500, color: 'var(--foreground, #0f172a)' }}>{order.code}</span>
|
|
51
|
+
<span style={{ fontSize: 12, color: 'var(--muted-foreground, #64748b)' }}>{order.state}</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 2, fontSize: 12, color: 'var(--muted-foreground, #64748b)' }}>
|
|
54
|
+
{date && <span>{date}</span>}
|
|
55
|
+
<span>{order.lines.length} item{order.lines.length !== 1 ? 's' : ''}</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Total + arrow */}
|
|
60
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
|
61
|
+
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--foreground, #0f172a)', fontVariantNumeric: 'tabular-nums' }}>{total}</span>
|
|
62
|
+
<ArrowUpRight style={{ width: 14, height: 14, color: 'var(--muted-foreground, #94a3b8)', opacity: hovered ? 1 : 0, transition: 'opacity 0.15s' }} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ImageIcon } from 'lucide-react';
|
|
3
|
+
import type { Product } from '../types';
|
|
4
|
+
|
|
5
|
+
interface ProductCardProps {
|
|
6
|
+
product: Product;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ProductCard({ product }: ProductCardProps) {
|
|
10
|
+
const price = `$${(product.price / 100).toFixed(2)}`;
|
|
11
|
+
const [hovered, setHovered] = useState(false);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
flexShrink: 0, width: 128, cursor: 'pointer', borderRadius: 8, overflow: 'hidden',
|
|
17
|
+
border: `1px solid ${hovered ? 'var(--foreground, #334155)' : 'var(--border, #e2e8f0)'}`,
|
|
18
|
+
background: 'var(--background, #fff)',
|
|
19
|
+
transition: 'border-color 0.15s',
|
|
20
|
+
}}
|
|
21
|
+
onClick={() => window.open(`/dashboard/products/${product.id}`, '_blank')}
|
|
22
|
+
onMouseEnter={() => setHovered(true)}
|
|
23
|
+
onMouseLeave={() => setHovered(false)}
|
|
24
|
+
>
|
|
25
|
+
<div style={{ height: 80, width: '100%', overflow: 'hidden', background: 'var(--muted, #f1f5f9)' }}>
|
|
26
|
+
{product.image ? (
|
|
27
|
+
<img src={product.image} alt={product.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
28
|
+
) : (
|
|
29
|
+
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.25 }}>
|
|
30
|
+
<ImageIcon style={{ width: 20, height: 20, color: 'var(--muted-foreground, #94a3b8)' }} />
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
<div style={{ padding: '8px 10px' }}>
|
|
35
|
+
<p style={{ fontSize: 11, fontWeight: 500, color: 'var(--foreground, #0f172a)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', margin: 0 }}>{product.name}</p>
|
|
36
|
+
<p style={{ fontSize: 11, color: 'var(--muted-foreground, #64748b)', margin: '2px 0 0' }}>{price}</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Plus, Trash2, MessageSquare } from 'lucide-react';
|
|
3
|
+
import type { SessionSummary } from '../hooks/use-session-list';
|
|
4
|
+
|
|
5
|
+
function formatDate(d: string | null): string {
|
|
6
|
+
if (!d) return '';
|
|
7
|
+
const date = new Date(d);
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const diffMs = now.getTime() - date.getTime();
|
|
10
|
+
const diffMins = Math.floor(diffMs / 60_000);
|
|
11
|
+
if (diffMins < 1) return 'Just now';
|
|
12
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
13
|
+
const diffHrs = Math.floor(diffMins / 60);
|
|
14
|
+
if (diffHrs < 24) return `${diffHrs}h ago`;
|
|
15
|
+
const diffDays = Math.floor(diffHrs / 24);
|
|
16
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
17
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SessionSidebarProps {
|
|
21
|
+
sessions: SessionSummary[];
|
|
22
|
+
activeSessionId: string;
|
|
23
|
+
onSelect: (sessionId: string) => void;
|
|
24
|
+
onNew: () => void;
|
|
25
|
+
onDelete: (sessionId: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function SessionSidebar({ sessions, activeSessionId, onSelect, onNew, onDelete }: SessionSidebarProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div style={{
|
|
31
|
+
display: 'flex', flexDirection: 'column', height: '100%',
|
|
32
|
+
borderRight: '1px solid var(--border, #e2e8f0)',
|
|
33
|
+
background: 'var(--muted, #f8fafc)',
|
|
34
|
+
width: 240, minWidth: 200, flexShrink: 0,
|
|
35
|
+
}}>
|
|
36
|
+
{/* New chat button */}
|
|
37
|
+
<div style={{ padding: 12, borderBottom: '1px solid var(--border, #e2e8f0)' }}>
|
|
38
|
+
<button
|
|
39
|
+
onClick={onNew}
|
|
40
|
+
style={{
|
|
41
|
+
display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'center',
|
|
42
|
+
gap: 8, padding: '8px 12px', borderRadius: 6,
|
|
43
|
+
border: '1px solid var(--border, #e2e8f0)',
|
|
44
|
+
background: 'var(--background, #fff)',
|
|
45
|
+
fontSize: 13, fontWeight: 500, cursor: 'pointer',
|
|
46
|
+
color: 'var(--foreground, #0f172a)',
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<Plus style={{ width: 16, height: 16 }} />
|
|
50
|
+
New Chat
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Session list */}
|
|
55
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
|
56
|
+
{sessions.length === 0 && (
|
|
57
|
+
<div style={{ textAlign: 'center', fontSize: 12, color: 'var(--muted-foreground, #64748b)', padding: '24px 0' }}>
|
|
58
|
+
No previous chats
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
{sessions.map(s => {
|
|
62
|
+
const isActive = s.sessionId === activeSessionId;
|
|
63
|
+
return <SessionItem key={s.sessionId} session={s} isActive={isActive} onSelect={onSelect} onDelete={onDelete} />;
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function SessionItem({ session, isActive, onSelect, onDelete }: {
|
|
71
|
+
session: SessionSummary;
|
|
72
|
+
isActive: boolean;
|
|
73
|
+
onSelect: (id: string) => void;
|
|
74
|
+
onDelete: (id: string) => void;
|
|
75
|
+
}) {
|
|
76
|
+
const [hovered, setHovered] = useState(false);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
style={{
|
|
81
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
82
|
+
padding: '8px 10px', borderRadius: 6, cursor: 'pointer',
|
|
83
|
+
marginBottom: 2,
|
|
84
|
+
background: isActive ? 'var(--muted, #e2e8f0)' : hovered ? 'rgba(0,0,0,0.03)' : 'transparent',
|
|
85
|
+
color: isActive ? 'var(--foreground, #0f172a)' : 'var(--muted-foreground, #64748b)',
|
|
86
|
+
transition: 'background 0.1s',
|
|
87
|
+
}}
|
|
88
|
+
onClick={() => onSelect(session.sessionId)}
|
|
89
|
+
onMouseEnter={() => setHovered(true)}
|
|
90
|
+
onMouseLeave={() => setHovered(false)}
|
|
91
|
+
>
|
|
92
|
+
<MessageSquare style={{ width: 14, height: 14, flexShrink: 0 }} />
|
|
93
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
94
|
+
<div style={{
|
|
95
|
+
fontSize: 12, fontWeight: 500,
|
|
96
|
+
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
97
|
+
}}>
|
|
98
|
+
{session.title || 'Untitled'}
|
|
99
|
+
</div>
|
|
100
|
+
<div style={{ fontSize: 10, opacity: 0.6 }}>
|
|
101
|
+
{formatDate(session.lastMessageAt || session.createdAt)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
{hovered && (
|
|
105
|
+
<button
|
|
106
|
+
onClick={e => { e.stopPropagation(); onDelete(session.sessionId); }}
|
|
107
|
+
style={{
|
|
108
|
+
padding: 4, borderRadius: 4, border: 'none',
|
|
109
|
+
background: 'transparent', cursor: 'pointer',
|
|
110
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
111
|
+
}}
|
|
112
|
+
title="Delete"
|
|
113
|
+
>
|
|
114
|
+
<Trash2 style={{ width: 12, height: 12, color: '#ef4444' }} />
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import type { ChatMessage } from '../types';
|
|
3
|
+
|
|
4
|
+
function generateSessionId(): string {
|
|
5
|
+
return `admin-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useAdminChat() {
|
|
9
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
10
|
+
const [input, setInput] = useState('');
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [sessionId, setSessionId] = useState<string>(generateSessionId);
|
|
13
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
17
|
+
}, [messages]);
|
|
18
|
+
|
|
19
|
+
const newConversation = useCallback(() => {
|
|
20
|
+
setMessages([]);
|
|
21
|
+
setSessionId(generateSessionId());
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
/** Load a previous session from the server. */
|
|
25
|
+
const loadSession = useCallback(async (sid: string) => {
|
|
26
|
+
try {
|
|
27
|
+
const sessionToken = localStorage.getItem('vendure-session-token');
|
|
28
|
+
const channelToken = localStorage.getItem('vendure-selected-channel-token');
|
|
29
|
+
const headers: Record<string, string> = {};
|
|
30
|
+
if (sessionToken) headers['Authorization'] = `Bearer ${sessionToken}`;
|
|
31
|
+
if (channelToken) headers['vendure-token'] = channelToken;
|
|
32
|
+
|
|
33
|
+
const res = await fetch(`/admin-ai-chat/sessions/${sid}`, { headers, credentials: 'include' });
|
|
34
|
+
if (!res.ok) return;
|
|
35
|
+
const session = await res.json();
|
|
36
|
+
if (!session?.messages) return;
|
|
37
|
+
|
|
38
|
+
setSessionId(sid);
|
|
39
|
+
setMessages(
|
|
40
|
+
session.messages.map((m: any, i: number) => ({
|
|
41
|
+
id: `${m.role[0]}-${i}-${Date.now()}`,
|
|
42
|
+
role: m.role,
|
|
43
|
+
content: m.content,
|
|
44
|
+
...(m.products?.length ? { products: m.products } : {}),
|
|
45
|
+
...(m.collections?.length ? { collections: m.collections } : {}),
|
|
46
|
+
...(m.orders?.length ? { orders: m.orders } : {}),
|
|
47
|
+
})),
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const sendMessage = useCallback(
|
|
55
|
+
async (text: string) => {
|
|
56
|
+
if (!text.trim() || isLoading) return;
|
|
57
|
+
|
|
58
|
+
const trimmed = text.trim();
|
|
59
|
+
setMessages(prev => [
|
|
60
|
+
...prev,
|
|
61
|
+
{ id: `u-${Date.now()}`, role: 'user', content: trimmed },
|
|
62
|
+
]);
|
|
63
|
+
setInput('');
|
|
64
|
+
setIsLoading(true);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const sessionToken = localStorage.getItem('vendure-session-token');
|
|
68
|
+
const channelToken = localStorage.getItem('vendure-selected-channel-token');
|
|
69
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
70
|
+
if (sessionToken) headers['Authorization'] = `Bearer ${sessionToken}`;
|
|
71
|
+
if (channelToken) headers['vendure-token'] = channelToken;
|
|
72
|
+
|
|
73
|
+
const res = await fetch('/admin-ai-chat/chat', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers,
|
|
76
|
+
credentials: 'include',
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
message: trimmed,
|
|
79
|
+
sessionId,
|
|
80
|
+
history: messages.map(m => ({ role: m.role, content: m.content })),
|
|
81
|
+
currentPage: window.location.pathname,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
|
|
87
|
+
// Server may return a sessionId (first turn creates it)
|
|
88
|
+
if (data.sessionId && data.sessionId !== sessionId) {
|
|
89
|
+
setSessionId(data.sessionId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setMessages(prev => [
|
|
93
|
+
...prev,
|
|
94
|
+
{
|
|
95
|
+
id: `a-${Date.now()}`,
|
|
96
|
+
role: 'assistant',
|
|
97
|
+
content: data.message ?? 'No response',
|
|
98
|
+
products: data.products,
|
|
99
|
+
collections: data.collections,
|
|
100
|
+
orders: data.orders,
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
// Handle navigation instruction from AI
|
|
105
|
+
if (data.navigateTo) {
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
const path = data.navigateTo.startsWith('/') ? `/dashboard${data.navigateTo}` : `/dashboard/${data.navigateTo}`;
|
|
108
|
+
window.location.href = path;
|
|
109
|
+
}, 1200);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
setMessages(prev => [
|
|
113
|
+
...prev,
|
|
114
|
+
{
|
|
115
|
+
id: `e-${Date.now()}`,
|
|
116
|
+
role: 'assistant',
|
|
117
|
+
content: `Error: ${err instanceof Error ? err.message : 'Request failed'}`,
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
} finally {
|
|
121
|
+
setIsLoading(false);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[isLoading, messages, sessionId],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
messages,
|
|
129
|
+
input,
|
|
130
|
+
setInput,
|
|
131
|
+
isLoading,
|
|
132
|
+
scrollRef,
|
|
133
|
+
sendMessage,
|
|
134
|
+
sessionId,
|
|
135
|
+
newConversation,
|
|
136
|
+
loadSession,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SessionSummary {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
messageCount: number;
|
|
7
|
+
model: string;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
lastMessageAt: string | null;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getHeaders(): Record<string, string> {
|
|
14
|
+
const headers: Record<string, string> = {};
|
|
15
|
+
const sessionToken = localStorage.getItem('vendure-session-token');
|
|
16
|
+
const channelToken = localStorage.getItem('vendure-selected-channel-token');
|
|
17
|
+
if (sessionToken) headers['Authorization'] = `Bearer ${sessionToken}`;
|
|
18
|
+
if (channelToken) headers['vendure-token'] = channelToken;
|
|
19
|
+
return headers;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useSessionList() {
|
|
23
|
+
const [sessions, setSessions] = useState<SessionSummary[]>([]);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
|
|
26
|
+
const refresh = useCallback(async () => {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch('/admin-ai-chat/sessions?take=50', {
|
|
30
|
+
headers: getHeaders(),
|
|
31
|
+
credentials: 'include',
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return;
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
setSessions(
|
|
36
|
+
(data.items ?? []).map((s: any) => ({
|
|
37
|
+
sessionId: s.sessionId,
|
|
38
|
+
title: s.title,
|
|
39
|
+
messageCount: s.messageCount,
|
|
40
|
+
model: s.model,
|
|
41
|
+
isActive: s.isActive,
|
|
42
|
+
lastMessageAt: s.lastMessageAt,
|
|
43
|
+
createdAt: s.createdAt,
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const deleteSession = useCallback(async (sessionId: string) => {
|
|
54
|
+
try {
|
|
55
|
+
await fetch(`/admin-ai-chat/sessions/${sessionId}`, {
|
|
56
|
+
method: 'DELETE',
|
|
57
|
+
headers: getHeaders(),
|
|
58
|
+
credentials: 'include',
|
|
59
|
+
});
|
|
60
|
+
setSessions(prev => prev.filter(s => s.sessionId !== sessionId));
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
67
|
+
|
|
68
|
+
return { sessions, loading, refresh, deleteSession };
|
|
69
|
+
}
|
package/src/dashboard/index.tsx
CHANGED
|
@@ -1,33 +1,13 @@
|
|
|
1
1
|
import { defineDashboardExtension } from '@vendure/dashboard';
|
|
2
2
|
import { AdminChatWidget } from './AdminChatWidget';
|
|
3
|
+
import { AiChatToolbarButton } from './components/AiChatToolbarButton';
|
|
4
|
+
import { EmbedAllButton } from './components/EmbedAllButton';
|
|
3
5
|
import React from 'react';
|
|
4
6
|
|
|
5
7
|
function AiChatPage() {
|
|
6
8
|
return (
|
|
7
|
-
<div style={{
|
|
8
|
-
|
|
9
|
-
flexDirection: 'column',
|
|
10
|
-
height: 'calc(100vh - 80px)',
|
|
11
|
-
padding: '24px',
|
|
12
|
-
}}>
|
|
13
|
-
<h1 style={{
|
|
14
|
-
fontSize: '24px',
|
|
15
|
-
fontWeight: 600,
|
|
16
|
-
marginBottom: '16px',
|
|
17
|
-
color: 'var(--color-foreground, #0f172a)',
|
|
18
|
-
}}>
|
|
19
|
-
AI Chat Assistant
|
|
20
|
-
</h1>
|
|
21
|
-
<div style={{
|
|
22
|
-
flex: 1,
|
|
23
|
-
minHeight: 0,
|
|
24
|
-
border: '1px solid var(--color-border, #e2e8f0)',
|
|
25
|
-
borderRadius: '8px',
|
|
26
|
-
overflow: 'hidden',
|
|
27
|
-
backgroundColor: 'var(--color-card, #fff)',
|
|
28
|
-
}}>
|
|
29
|
-
<AdminChatWidget />
|
|
30
|
-
</div>
|
|
9
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 64px)', minHeight: 0 }}>
|
|
10
|
+
<AdminChatWidget />
|
|
31
11
|
</div>
|
|
32
12
|
);
|
|
33
13
|
}
|
|
@@ -52,4 +32,18 @@ defineDashboardExtension({
|
|
|
52
32
|
order: 150,
|
|
53
33
|
},
|
|
54
34
|
],
|
|
35
|
+
actionBarItems: [
|
|
36
|
+
{
|
|
37
|
+
pageId: 'global-setting-detail',
|
|
38
|
+
id: 'embed-all-button',
|
|
39
|
+
component: () => <EmbedAllButton />,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
toolbarItems: [
|
|
43
|
+
{
|
|
44
|
+
id: 'ai-chat-toolbar',
|
|
45
|
+
component: AiChatToolbarButton,
|
|
46
|
+
position: { itemId: 'alerts', order: 'before' },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
55
49
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ─── Types (mirror backend response) ────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface Product {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
price: number;
|
|
8
|
+
image: string | null;
|
|
9
|
+
variantId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Collection {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OrderLine {
|
|
19
|
+
productName: string;
|
|
20
|
+
quantity: number;
|
|
21
|
+
image: string | null;
|
|
22
|
+
linePriceWithTax: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Order {
|
|
26
|
+
id: string;
|
|
27
|
+
code: string;
|
|
28
|
+
state: string;
|
|
29
|
+
total: number;
|
|
30
|
+
orderPlacedAt: string | null;
|
|
31
|
+
lines: OrderLine[];
|
|
32
|
+
fulfillments: Array<{
|
|
33
|
+
state: string;
|
|
34
|
+
method: string;
|
|
35
|
+
trackingCode: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatMessage {
|
|
40
|
+
id: string;
|
|
41
|
+
role: 'user' | 'assistant';
|
|
42
|
+
content: string;
|
|
43
|
+
products?: Product[];
|
|
44
|
+
collections?: Collection[];
|
|
45
|
+
orders?: Order[];
|
|
46
|
+
}
|