@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
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (C) 2025 Rahul Yadav
|
|
2
|
+
|
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
|
4
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
5
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
(at your option) any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful,
|
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
GNU Affero General Public License for more details.
|
|
12
|
+
|
|
13
|
+
You should have received a copy of the GNU Affero General Public License
|
|
14
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rahul_vendure/ai-chat-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI chat assistant widget for the Vendure admin dashboard — adds a floating chat panel to the React-based Vendure 3.x dashboard",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
16
|
"src/dashboard",
|
|
17
|
-
"README.md"
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"build": "tsc",
|
|
@@ -31,13 +32,13 @@
|
|
|
31
32
|
"react"
|
|
32
33
|
],
|
|
33
34
|
"author": "Rahul Yadav",
|
|
34
|
-
"license": "
|
|
35
|
+
"license": "AGPL-3.0",
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
|
37
38
|
},
|
|
38
39
|
"repository": {
|
|
39
40
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/Ryrahul/Vendure-ai.git",
|
|
41
|
+
"url": "git+https://github.com/Ryrahul/Vendure-ai.git",
|
|
41
42
|
"directory": "packages/ai-chat-dashboard"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
@@ -45,14 +46,15 @@
|
|
|
45
46
|
"@vendure/dashboard": ">=3.5.0"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
49
|
+
"lucide-react": "^1.8.0",
|
|
48
50
|
"react-markdown": "^10.1.0",
|
|
49
51
|
"remark-gfm": "^4.0.1"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
54
|
+
"@types/react": "^19.2.0",
|
|
52
55
|
"@vendure/core": "3.5.4",
|
|
53
56
|
"@vendure/dashboard": "3.5.4",
|
|
54
57
|
"react": "^19.2.0",
|
|
55
|
-
"@types/react": "^19.2.0",
|
|
56
58
|
"typescript": "^5.8.0"
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -1,291 +1,135 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { useAdminChat } from './hooks/use-admin-chat';
|
|
3
|
+
import { useSessionList } from './hooks/use-session-list';
|
|
4
|
+
import { SessionSidebar } from './components/SessionSidebar';
|
|
5
|
+
import { ChatEmptyState } from './components/ChatEmptyState';
|
|
6
|
+
import { ChatMessageBubble } from './components/ChatMessageBubble';
|
|
7
|
+
import { ChatInput } from './components/ChatInput';
|
|
8
|
+
import { Bot } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
function LoadingDots() {
|
|
11
|
+
return (
|
|
12
|
+
<div style={{
|
|
13
|
+
display: 'flex', gap: 12, padding: 16, margin: '16px 0',
|
|
14
|
+
borderRadius: 12, border: '1px solid var(--border, #e2e8f0)',
|
|
15
|
+
background: 'var(--background, #fff)',
|
|
16
|
+
}}>
|
|
17
|
+
<div style={{
|
|
18
|
+
width: 28, height: 28, borderRadius: '50%', flexShrink: 0,
|
|
19
|
+
background: 'var(--foreground, #0f172a)', color: 'var(--background, #fff)',
|
|
20
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
21
|
+
}}>
|
|
22
|
+
<Bot style={{ width: 16, height: 16 }} />
|
|
23
|
+
</div>
|
|
24
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingTop: 6 }}>
|
|
25
|
+
<span className="ac-dot" />
|
|
26
|
+
<span className="ac-dot" style={{ animationDelay: '0.15s' }} />
|
|
27
|
+
<span className="ac-dot" style={{ animationDelay: '0.3s' }} />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
// ─── Component ──────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
33
|
export function AdminChatWidget() {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const {
|
|
35
|
+
messages, input, setInput, isLoading, scrollRef,
|
|
36
|
+
sendMessage, sessionId, newConversation, loadSession,
|
|
37
|
+
} = useAdminChat();
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
const { sessions, refresh: refreshSessions, deleteSession } = useSessionList();
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
41
|
+
const handleNewChat = useCallback(() => {
|
|
42
|
+
newConversation();
|
|
43
|
+
}, [newConversation]);
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
setInput('');
|
|
43
|
-
setIsLoading(true);
|
|
44
|
-
try {
|
|
45
|
-
const sessionToken = localStorage.getItem('vendure-session-token');
|
|
46
|
-
const channelToken = localStorage.getItem('vendure-selected-channel-token');
|
|
47
|
-
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
48
|
-
if (sessionToken) headers['Authorization'] = `Bearer ${sessionToken}`;
|
|
49
|
-
if (channelToken) headers['vendure-token'] = channelToken;
|
|
45
|
+
const handleSelectSession = useCallback(async (sid: string) => {
|
|
46
|
+
await loadSession(sid);
|
|
47
|
+
}, [loadSession]);
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} finally { setIsLoading(false); }
|
|
63
|
-
}
|
|
49
|
+
const handleDeleteSession = useCallback(async (sid: string) => {
|
|
50
|
+
await deleteSession(sid);
|
|
51
|
+
if (sid === sessionId) {
|
|
52
|
+
newConversation();
|
|
53
|
+
}
|
|
54
|
+
}, [deleteSession, sessionId, newConversation]);
|
|
55
|
+
|
|
56
|
+
const handleSend = useCallback(async (text: string) => {
|
|
57
|
+
await sendMessage(text);
|
|
58
|
+
setTimeout(() => refreshSessions(), 500);
|
|
59
|
+
}, [sendMessage, refreshSessions]);
|
|
64
60
|
|
|
65
61
|
return (
|
|
66
|
-
<div
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
62
|
+
<div style={{ display: 'flex', height: '100%', minHeight: 0, background: 'var(--background, #fff)' }}>
|
|
63
|
+
{/* Sidebar */}
|
|
64
|
+
<SessionSidebar
|
|
65
|
+
sessions={sessions}
|
|
66
|
+
activeSessionId={sessionId}
|
|
67
|
+
onSelect={handleSelectSession}
|
|
68
|
+
onNew={handleNewChat}
|
|
69
|
+
onDelete={handleDeleteSession}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{/* Main chat area */}
|
|
73
|
+
<div style={{ display: 'flex', flex: 1, minHeight: 0, flexDirection: 'column' }}>
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div style={{
|
|
76
|
+
borderBottom: '1px solid var(--border, #e2e8f0)',
|
|
77
|
+
padding: '10px 20px',
|
|
78
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
79
|
+
background: 'var(--background, #fff)',
|
|
80
|
+
flexShrink: 0,
|
|
81
|
+
}}>
|
|
82
|
+
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--foreground, #0f172a)' }}>AI Assistant</span>
|
|
83
|
+
{messages.length > 0 && (
|
|
84
|
+
<span style={{ fontSize: 10, color: 'var(--muted-foreground, #94a3b8)', fontFamily: 'monospace' }}>
|
|
85
|
+
{sessionId.slice(0, 20)}...
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
85
89
|
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
{/* Messages Area */}
|
|
91
|
+
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
|
92
|
+
{messages.length === 0 ? (
|
|
93
|
+
<ChatEmptyState onSend={handleSend} />
|
|
94
|
+
) : (
|
|
95
|
+
<div style={{ maxWidth: 768, margin: '0 auto', padding: '0 20px' }}>
|
|
96
|
+
<div style={{ padding: '16px 0' }}>
|
|
97
|
+
{messages.map(msg => (
|
|
98
|
+
<ChatMessageBubble key={msg.id} message={msg} />
|
|
99
|
+
))}
|
|
100
|
+
{isLoading && <LoadingDots />}
|
|
101
|
+
<div ref={scrollRef} style={{ height: 32 }} />
|
|
94
102
|
</div>
|
|
95
|
-
{/* Product cards */}
|
|
96
|
-
{msg.products && msg.products.length > 0 && (
|
|
97
|
-
<div className="ac-cards">
|
|
98
|
-
{msg.products.map(p => <ProductCard key={p.id} product={p} />)}
|
|
99
|
-
</div>
|
|
100
|
-
)}
|
|
101
|
-
{/* Order cards */}
|
|
102
|
-
{msg.orders && msg.orders.length > 0 && (
|
|
103
|
-
<div className="ac-order-cards">
|
|
104
|
-
{msg.orders.map(o => <OrderCard key={o.id} order={o} />)}
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
103
|
</div>
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
110
106
|
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
{/* Input */}
|
|
108
|
+
<div style={{
|
|
109
|
+
borderTop: '1px solid var(--border, #e2e8f0)',
|
|
110
|
+
background: 'var(--background, #fff)',
|
|
111
|
+
flexShrink: 0,
|
|
112
|
+
}}>
|
|
113
|
+
<div style={{ maxWidth: 768, margin: '0 auto' }}>
|
|
114
|
+
<ChatInput
|
|
115
|
+
value={input}
|
|
116
|
+
onChange={setInput}
|
|
117
|
+
onSubmit={() => handleSend(input)}
|
|
118
|
+
disabled={isLoading}
|
|
119
|
+
/>
|
|
115
120
|
</div>
|
|
116
|
-
|
|
117
|
-
<div ref={scrollRef} />
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<form className="ac-bar" onSubmit={e => { e.preventDefault(); sendMessage(input); }}>
|
|
121
|
-
<input className="ac-input" value={input} onChange={e => setInput(e.target.value)}
|
|
122
|
-
placeholder="Ask about products, orders, customers..." disabled={isLoading} />
|
|
123
|
-
<button type="submit" className="ac-send" disabled={isLoading || !input.trim()}>
|
|
124
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
125
|
-
<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>
|
|
126
|
-
</svg>
|
|
127
|
-
</button>
|
|
128
|
-
</form>
|
|
129
|
-
<style>{STYLES}</style>
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ─── Sub-components ─────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
function ProductCard({ product }: { product: Product }) {
|
|
137
|
-
const price = `$${(product.price / 100).toFixed(2)}`;
|
|
138
|
-
return (
|
|
139
|
-
<div className="ac-pcard" onClick={() => window.open(`/dashboard/products/${product.id}`, '_blank')}>
|
|
140
|
-
<div className="ac-pcard-img">
|
|
141
|
-
{product.image
|
|
142
|
-
? <img src={product.image} alt={product.name} />
|
|
143
|
-
: <div className="ac-pcard-noimg">
|
|
144
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
|
|
145
|
-
</div>}
|
|
146
|
-
</div>
|
|
147
|
-
<div className="ac-pcard-body">
|
|
148
|
-
<div className="ac-pcard-name">{product.name}</div>
|
|
149
|
-
<div className="ac-pcard-price">{price}</div>
|
|
121
|
+
</div>
|
|
150
122
|
</div>
|
|
151
|
-
</div>
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
123
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
</div>
|
|
165
|
-
<div className="ac-ocard-meta">{date} · {total} · {order.lines.length} item(s)</div>
|
|
166
|
-
{order.lines.length > 0 && (
|
|
167
|
-
<div className="ac-ocard-lines">
|
|
168
|
-
{order.lines.slice(0, 3).map((l, i) => (
|
|
169
|
-
<div key={i} className="ac-ocard-line">
|
|
170
|
-
{l.image && <img src={l.image} alt={l.productName} className="ac-ocard-line-img" />}
|
|
171
|
-
<span className="ac-ocard-line-name">{l.quantity}x {l.productName}</span>
|
|
172
|
-
</div>
|
|
173
|
-
))}
|
|
174
|
-
{order.lines.length > 3 && <div className="ac-ocard-more">+{order.lines.length - 3} more</div>}
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
124
|
+
<style>{`
|
|
125
|
+
.ac-dot {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
width: 5px; height: 5px; border-radius: 50%;
|
|
128
|
+
background: var(--muted-foreground, #94a3b8);
|
|
129
|
+
animation: ac-bounce 0.6s infinite alternate;
|
|
130
|
+
}
|
|
131
|
+
@keyframes ac-bounce { to { opacity: 0.3; transform: translateY(-4px); } }
|
|
132
|
+
`}</style>
|
|
177
133
|
</div>
|
|
178
134
|
);
|
|
179
135
|
}
|
|
180
|
-
|
|
181
|
-
// ─── Styles ─────────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
const STYLES = `
|
|
184
|
-
.ac-root { display:flex; flex-direction:column; height:100%; min-height:0; font-family:var(--font-sans,system-ui,-apple-system,sans-serif); }
|
|
185
|
-
.ac-messages { flex:1; overflow-y:auto; padding:24px; display:flex; flex-direction:column; gap:16px; }
|
|
186
|
-
|
|
187
|
-
/* Empty */
|
|
188
|
-
.ac-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; gap:12px; }
|
|
189
|
-
.ac-empty-icon { color:var(--color-muted-foreground,#94a3b8); }
|
|
190
|
-
.ac-empty-title { font-size:18px; font-weight:600; color:var(--color-foreground,#0f172a); }
|
|
191
|
-
.ac-empty-sub { font-size:14px; color:var(--color-muted-foreground,#64748b); }
|
|
192
|
-
.ac-chips { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; margin-top:8px; }
|
|
193
|
-
.ac-chip {
|
|
194
|
-
padding:7px 14px; border-radius:99px; font-size:13px; cursor:pointer;
|
|
195
|
-
border:1px solid var(--color-border,#e2e8f0); background:var(--color-background,#fff);
|
|
196
|
-
color:var(--color-foreground,#334155); transition:all .15s;
|
|
197
|
-
}
|
|
198
|
-
.ac-chip:hover { border-color:var(--color-primary,#6366f1); color:var(--color-primary,#6366f1); background:color-mix(in srgb,var(--color-primary,#6366f1) 6%,transparent); }
|
|
199
|
-
|
|
200
|
-
/* Rows */
|
|
201
|
-
.ac-row { display:flex; gap:10px; align-items:flex-start; }
|
|
202
|
-
.ac-row--user { justify-content:flex-end; }
|
|
203
|
-
.ac-row--assistant { justify-content:flex-start; }
|
|
204
|
-
.ac-msg-col { display:flex; flex-direction:column; gap:10px; max-width:88%; min-width:0; }
|
|
205
|
-
.ac-avatar {
|
|
206
|
-
width:28px; height:28px; border-radius:8px; flex-shrink:0; margin-top:2px;
|
|
207
|
-
background:var(--color-primary,#6366f1); color:#fff;
|
|
208
|
-
display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; letter-spacing:.5px;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* Bubbles */
|
|
212
|
-
.ac-bubble { padding:10px 14px; border-radius:14px; font-size:14px; line-height:1.55; word-break:break-word; }
|
|
213
|
-
.ac-bubble--user { background:var(--color-primary,#6366f1); color:var(--color-primary-foreground,#fff); border-bottom-right-radius:4px; }
|
|
214
|
-
.ac-bubble--assistant { background:var(--color-card,#fff); color:var(--color-foreground,#0f172a); border:1px solid var(--color-border,#e2e8f0); border-bottom-left-radius:4px; padding:12px 16px; }
|
|
215
|
-
|
|
216
|
-
/* Markdown */
|
|
217
|
-
.ac-md { font-size:14px; line-height:1.6; }
|
|
218
|
-
.ac-md > *:first-child { margin-top:0 !important; }
|
|
219
|
-
.ac-md > *:last-child { margin-bottom:0 !important; }
|
|
220
|
-
.ac-md p { margin:0 0 8px; }
|
|
221
|
-
.ac-md p:last-child { margin:0; }
|
|
222
|
-
.ac-md strong { font-weight:600; }
|
|
223
|
-
.ac-md ul,.ac-md ol { margin:4px 0 8px; padding-left:20px; }
|
|
224
|
-
.ac-md li { margin:2px 0; }
|
|
225
|
-
.ac-md code { background:var(--color-muted,#f1f5f9); padding:1px 5px; border-radius:4px; font-size:12.5px; font-family:var(--font-mono,ui-monospace,monospace); }
|
|
226
|
-
.ac-md pre { background:var(--color-muted,#f1f5f9); padding:12px; border-radius:8px; overflow-x:auto; margin:8px 0; }
|
|
227
|
-
.ac-md pre code { background:none; padding:0; }
|
|
228
|
-
.ac-md table { border-collapse:collapse; width:100%; margin:10px 0; font-size:13px; border:1px solid var(--color-border,#e2e8f0); border-radius:8px; overflow:hidden; }
|
|
229
|
-
.ac-md thead { background:var(--color-muted,#f1f5f9); }
|
|
230
|
-
.ac-md th { text-align:left; padding:8px 12px; font-weight:600; font-size:12px; color:var(--color-muted-foreground,#64748b); text-transform:uppercase; letter-spacing:.3px; border-bottom:2px solid var(--color-border,#e2e8f0); }
|
|
231
|
-
.ac-md td { padding:8px 12px; border-bottom:1px solid var(--color-border,#f1f5f9); }
|
|
232
|
-
.ac-md tbody tr:last-child td { border-bottom:none; }
|
|
233
|
-
.ac-md tbody tr:hover { background:var(--color-muted,#f8fafc); }
|
|
234
|
-
|
|
235
|
-
/* Product cards row */
|
|
236
|
-
.ac-cards { display:flex; gap:10px; overflow-x:auto; padding:2px 0; }
|
|
237
|
-
.ac-pcard {
|
|
238
|
-
flex-shrink:0; width:160px; border-radius:10px; overflow:hidden; cursor:pointer;
|
|
239
|
-
border:1px solid var(--color-border,#e2e8f0); background:var(--color-card,#fff);
|
|
240
|
-
transition:all .15s; box-shadow:0 1px 2px rgba(0,0,0,.04);
|
|
241
|
-
}
|
|
242
|
-
.ac-pcard:hover { border-color:var(--color-primary,#6366f1); box-shadow:0 2px 8px rgba(99,102,241,.12); transform:translateY(-1px); }
|
|
243
|
-
.ac-pcard-img { width:100%; height:120px; overflow:hidden; background:var(--color-muted,#f1f5f9); }
|
|
244
|
-
.ac-pcard-img img { width:100%; height:100%; object-fit:cover; }
|
|
245
|
-
.ac-pcard-noimg { width:100%; height:100%; display:flex; align-items:center; justify-content:center; color:var(--color-muted-foreground,#94a3b8); }
|
|
246
|
-
.ac-pcard-body { padding:8px 10px; }
|
|
247
|
-
.ac-pcard-name { font-size:13px; font-weight:500; color:var(--color-foreground,#0f172a); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
248
|
-
.ac-pcard-price { font-size:13px; font-weight:600; color:var(--color-primary,#6366f1); margin-top:2px; }
|
|
249
|
-
|
|
250
|
-
/* Order cards */
|
|
251
|
-
.ac-order-cards { display:flex; flex-direction:column; gap:8px; }
|
|
252
|
-
.ac-ocard {
|
|
253
|
-
border-radius:10px; padding:12px 14px; cursor:pointer;
|
|
254
|
-
border:1px solid var(--color-border,#e2e8f0); background:var(--color-card,#fff);
|
|
255
|
-
transition:all .15s; box-shadow:0 1px 2px rgba(0,0,0,.04);
|
|
256
|
-
}
|
|
257
|
-
.ac-ocard:hover { border-color:var(--color-primary,#6366f1); box-shadow:0 2px 8px rgba(99,102,241,.12); }
|
|
258
|
-
.ac-ocard-header { display:flex; align-items:center; gap:8px; margin-bottom:4px; }
|
|
259
|
-
.ac-ocard-code { font-size:13px; font-weight:600; font-family:var(--font-mono,ui-monospace,monospace); color:var(--color-foreground,#0f172a); }
|
|
260
|
-
.ac-ocard-state { font-size:11px; font-weight:600; padding:2px 8px; border-radius:99px; color:#fff; text-transform:uppercase; letter-spacing:.3px; }
|
|
261
|
-
.ac-ocard-meta { font-size:12px; color:var(--color-muted-foreground,#64748b); margin-bottom:6px; }
|
|
262
|
-
.ac-ocard-lines { display:flex; flex-direction:column; gap:4px; }
|
|
263
|
-
.ac-ocard-line { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--color-foreground,#334155); }
|
|
264
|
-
.ac-ocard-line-img { width:28px; height:28px; border-radius:4px; object-fit:cover; }
|
|
265
|
-
.ac-ocard-line-name { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
266
|
-
.ac-ocard-more { font-size:11px; color:var(--color-muted-foreground,#94a3b8); padding-left:36px; }
|
|
267
|
-
|
|
268
|
-
/* Input */
|
|
269
|
-
.ac-bar { display:flex; gap:8px; padding:16px 24px; border-top:1px solid var(--color-border,#e2e8f0); background:var(--color-background,#fff); }
|
|
270
|
-
.ac-input {
|
|
271
|
-
flex:1; padding:10px 14px; border-radius:10px; font-size:14px; outline:none;
|
|
272
|
-
border:1px solid var(--color-border,#e2e8f0); background:var(--color-background,#fff);
|
|
273
|
-
color:var(--color-foreground,#0f172a); transition:border-color .15s,box-shadow .15s;
|
|
274
|
-
}
|
|
275
|
-
.ac-input:focus { border-color:var(--color-primary,#6366f1); box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary,#6366f1) 12%,transparent); }
|
|
276
|
-
.ac-input:disabled { opacity:.5; }
|
|
277
|
-
.ac-send {
|
|
278
|
-
width:40px; height:40px; border-radius:10px; border:none; cursor:pointer;
|
|
279
|
-
background:var(--color-primary,#6366f1); color:#fff;
|
|
280
|
-
display:flex; align-items:center; justify-content:center; transition:opacity .15s;
|
|
281
|
-
}
|
|
282
|
-
.ac-send:hover:not(:disabled) { opacity:.85; }
|
|
283
|
-
.ac-send:disabled { opacity:.35; cursor:not-allowed; }
|
|
284
|
-
|
|
285
|
-
/* Dots */
|
|
286
|
-
.ac-dots { display:inline-flex; gap:4px; align-items:center; height:20px; }
|
|
287
|
-
.ac-dots span { width:6px; height:6px; border-radius:50%; background:var(--color-muted-foreground,#94a3b8); animation:ac-bounce .6s infinite alternate; }
|
|
288
|
-
.ac-dots span:nth-child(2) { animation-delay:.15s; }
|
|
289
|
-
.ac-dots span:nth-child(3) { animation-delay:.3s; }
|
|
290
|
-
@keyframes ac-bounce { to { opacity:.3; transform:translateY(-4px); } }
|
|
291
|
-
`;
|