@rahul_vendure/ai-chat-dashboard 0.1.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/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @rahul_vendure/ai-chat-dashboard
2
+
3
+ AI chat assistant widget for the [Vendure](https://www.vendure.io/) admin dashboard. Adds an "AI Chat" page to the React-based Vendure 3.x dashboard where store admins can query products, orders, customers, and collections using natural language.
4
+
5
+ ## Features
6
+
7
+ - **Dashboard Extension** — Adds an "AI Chat" page under its own "AI" section in the sidebar
8
+ - **Product Cards** — Search results render as visual cards with images, prices — click to open in the catalog
9
+ - **Order Cards** — Orders render with status badges, line items with thumbnails — click to open order detail
10
+ - **Semantic Search** — "products for developers", "gaming gear" — uses pgvector embeddings
11
+ - **Customer Lookup** — Search customers by name or email with order stats
12
+ - **Collection Browser** — List and search product categories
13
+ - **Markdown Rendering** — AI responses rendered with `react-markdown` + GitHub-flavored tables
14
+ - **Multi-turn Conversations** — Conversation history maintained across messages
15
+ - **Dashboard Theme** — Uses Vendure dashboard CSS variables, matches light/dark theme
16
+
17
+ ## Requirements
18
+
19
+ - Vendure `>=3.0.0` with the React-based `@vendure/dashboard` (`>=3.5.0`)
20
+ - [`@rahul_vendure/ai-chat-plugin`](https://www.npmjs.com/package/@rahul_vendure/ai-chat-plugin) — provides the backend AI service and REST endpoint
21
+ - `DashboardPlugin` must be configured in your Vendure config
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @rahul_vendure/ai-chat-plugin @rahul_vendure/ai-chat-dashboard
27
+ ```
28
+
29
+ ## Setup
30
+
31
+ Add both plugins to your Vendure config:
32
+
33
+ ```ts
34
+ import { AiAssistantPlugin } from '@rahul_vendure/ai-chat-plugin';
35
+ import { AiChatDashboardPlugin } from '@rahul_vendure/ai-chat-dashboard';
36
+ import { DashboardPlugin } from '@vendure/dashboard/plugin';
37
+
38
+ export const config: VendureConfig = {
39
+ plugins: [
40
+ AiAssistantPlugin.init({
41
+ openaiApiKey: process.env.OPENAI_API_KEY!,
42
+ }),
43
+ AiChatDashboardPlugin,
44
+ DashboardPlugin.init({
45
+ route: 'dashboard',
46
+ appDir: path.join(__dirname, '../dist/dashboard'),
47
+ }),
48
+ ],
49
+ };
50
+ ```
51
+
52
+ ### Workspace / Monorepo Setup
53
+
54
+ If you're using npm workspaces (the plugin is symlinked), Vendure's built-in dashboard extension scanner may not detect symlinked packages. Add this workaround to your `vite.config.mts`:
55
+
56
+ ```ts
57
+ import { vendureDashboardPlugin } from '@vendure/dashboard/vite';
58
+ import { readdirSync, existsSync, lstatSync } from 'fs';
59
+ import { join, resolve } from 'path';
60
+ import { pathToFileURL } from 'url';
61
+ import { defineConfig, Plugin } from 'vite';
62
+
63
+ function workspaceDashboardExtensions(): Plugin {
64
+ const resolvedId = `\0virtual:dashboard-extensions`;
65
+ const packagesDir = resolve(__dirname, '../../packages');
66
+
67
+ function discoverDashboardExtensions(): string[] {
68
+ const extensions: string[] = [];
69
+ if (!existsSync(packagesDir)) return extensions;
70
+ for (const entry of readdirSync(packagesDir)) {
71
+ const pkgDir = join(packagesDir, entry);
72
+ try { if (!lstatSync(pkgDir).isDirectory()) continue; } catch { continue; }
73
+ const dashboardEntry = join(pkgDir, 'src/dashboard/index.tsx');
74
+ if (existsSync(dashboardEntry)) extensions.push(dashboardEntry);
75
+ }
76
+ return extensions;
77
+ }
78
+
79
+ return {
80
+ name: 'vendure:workspace-dashboard-extensions',
81
+ enforce: 'post',
82
+ transform(code, id) {
83
+ if (id === resolvedId) {
84
+ const extensions = discoverDashboardExtensions();
85
+ const imports = extensions
86
+ .map(ext => `await import('${pathToFileURL(ext).href}');`)
87
+ .join('\n ');
88
+ return {
89
+ code: `export async function runDashboardExtensions() {\n ${imports}\n}`,
90
+ map: null,
91
+ };
92
+ }
93
+ },
94
+ };
95
+ }
96
+
97
+ export default defineConfig({
98
+ plugins: [
99
+ vendureDashboardPlugin({ /* ... */ }),
100
+ workspaceDashboardExtensions(),
101
+ ],
102
+ });
103
+ ```
104
+
105
+ ## What It Looks Like
106
+
107
+ After setup, you'll see an **AI** section in the dashboard sidebar with an **AI Chat** page. The chat supports:
108
+
109
+ - **Product search** — "do we have laptops?" → product cards with images
110
+ - **Semantic search** — "products for developers" → vector search results
111
+ - **Order lookup** — "show orders by john" → order cards with status badges
112
+ - **Customer search** — "find customers named rahul" → customer details with order stats
113
+ - **Collection browser** — "list collections" → formatted table
114
+
115
+ Product cards link to `/dashboard/products/{id}` and order cards link to `/dashboard/orders/{id}`.
116
+
117
+ ## Architecture
118
+
119
+ This is a **dashboard-only** package — it contains no backend logic. The architecture is:
120
+
121
+ | Package | Role |
122
+ |---------|------|
123
+ | `@rahul_vendure/ai-chat-plugin` | Backend: AI service, tools, REST endpoint at `/admin-ai-chat/chat` |
124
+ | `@rahul_vendure/ai-chat-dashboard` | Frontend: Dashboard extension with chat UI, product/order cards |
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Adds an AI chat assistant widget to the Vendure admin dashboard.
3
+ *
4
+ * This is a **dashboard-only** plugin — it contains no backend logic.
5
+ * The backend REST endpoint (`POST /admin-ai-chat/chat`) is provided by
6
+ * `@rahul_vendure/ai-chat-plugin`, which must also be installed.
7
+ *
8
+ * Usage:
9
+ * ```ts
10
+ * import { AiAssistantPlugin } from '@rahul_vendure/ai-chat-plugin';
11
+ * import { AiChatDashboardPlugin } from '@rahul_vendure/ai-chat-dashboard';
12
+ *
13
+ * const config: VendureConfig = {
14
+ * plugins: [
15
+ * AiAssistantPlugin.init({ openaiApiKey: '...' }),
16
+ * AiChatDashboardPlugin,
17
+ * ],
18
+ * };
19
+ * ```
20
+ */
21
+ export declare class AiChatDashboardPlugin {
22
+ }
23
+ //# sourceMappingURL=ai-chat-dashboard.plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-chat-dashboard.plugin.d.ts","sourceRoot":"","sources":["../src/ai-chat-dashboard.plugin.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAKa,qBAAqB;CAAG"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AiChatDashboardPlugin = void 0;
10
+ const core_1 = require("@vendure/core");
11
+ /**
12
+ * Adds an AI chat assistant widget to the Vendure admin dashboard.
13
+ *
14
+ * This is a **dashboard-only** plugin — it contains no backend logic.
15
+ * The backend REST endpoint (`POST /admin-ai-chat/chat`) is provided by
16
+ * `@rahul_vendure/ai-chat-plugin`, which must also be installed.
17
+ *
18
+ * Usage:
19
+ * ```ts
20
+ * import { AiAssistantPlugin } from '@rahul_vendure/ai-chat-plugin';
21
+ * import { AiChatDashboardPlugin } from '@rahul_vendure/ai-chat-dashboard';
22
+ *
23
+ * const config: VendureConfig = {
24
+ * plugins: [
25
+ * AiAssistantPlugin.init({ openaiApiKey: '...' }),
26
+ * AiChatDashboardPlugin,
27
+ * ],
28
+ * };
29
+ * ```
30
+ */
31
+ let AiChatDashboardPlugin = class AiChatDashboardPlugin {
32
+ };
33
+ exports.AiChatDashboardPlugin = AiChatDashboardPlugin;
34
+ exports.AiChatDashboardPlugin = AiChatDashboardPlugin = __decorate([
35
+ (0, core_1.VendurePlugin)({
36
+ imports: [core_1.PluginCommonModule],
37
+ dashboard: './dashboard/index.tsx',
38
+ compatibility: '^3.0.0',
39
+ })
40
+ ], AiChatDashboardPlugin);
41
+ //# sourceMappingURL=ai-chat-dashboard.plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-chat-dashboard.plugin.js","sourceRoot":"","sources":["../src/ai-chat-dashboard.plugin.ts"],"names":[],"mappings":";;;;;;;;;AAAA,wCAAkE;AAElE;;;;;;;;;;;;;;;;;;;GAmBG;AAMI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;CAAG,CAAA;AAAxB,sDAAqB;gCAArB,qBAAqB;IALjC,IAAA,oBAAa,EAAC;QACX,OAAO,EAAE,CAAC,yBAAkB,CAAC;QAC7B,SAAS,EAAE,uBAAuB;QAClC,aAAa,EAAE,QAAQ;KAC1B,CAAC;GACW,qBAAqB,CAAG"}
@@ -0,0 +1,2 @@
1
+ export { AiChatDashboardPlugin } from './ai-chat-dashboard.plugin';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AiChatDashboardPlugin = void 0;
4
+ var ai_chat_dashboard_plugin_1 = require("./ai-chat-dashboard.plugin");
5
+ Object.defineProperty(exports, "AiChatDashboardPlugin", { enumerable: true, get: function () { return ai_chat_dashboard_plugin_1.AiChatDashboardPlugin; } });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uEAAmE;AAA1D,iIAAA,qBAAqB,OAAA"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@rahul_vendure/ai-chat-dashboard",
3
+ "version": "0.1.0",
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
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js"
11
+ },
12
+ "./dashboard": "./src/dashboard/index.tsx"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src/dashboard",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch"
22
+ },
23
+ "keywords": [
24
+ "vendure",
25
+ "vendure-plugin",
26
+ "ai",
27
+ "chat",
28
+ "dashboard",
29
+ "admin",
30
+ "admin-ui",
31
+ "react"
32
+ ],
33
+ "author": "Rahul Yadav",
34
+ "license": "MIT",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/Ryrahul/Vendure-ai.git",
41
+ "directory": "packages/ai-chat-dashboard"
42
+ },
43
+ "peerDependencies": {
44
+ "@vendure/core": ">=3.0.0",
45
+ "@vendure/dashboard": ">=3.5.0"
46
+ },
47
+ "dependencies": {
48
+ "react-markdown": "^10.1.0",
49
+ "remark-gfm": "^4.0.1"
50
+ },
51
+ "devDependencies": {
52
+ "@vendure/core": "3.5.4",
53
+ "@vendure/dashboard": "3.5.4",
54
+ "react": "^19.2.0",
55
+ "@types/react": "^19.2.0",
56
+ "typescript": "^5.8.0"
57
+ }
58
+ }
@@ -0,0 +1,291 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import Markdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+
5
+ // ─── Types (mirror backend response) ────────────────────────────────
6
+
7
+ interface Product { id: string; name: string; slug: string; price: number; image: string | null; variantId: string; }
8
+ interface Collection { id: string; name: string; slug: string; }
9
+ interface OrderLine { productName: string; quantity: number; image: string | null; linePriceWithTax: number; }
10
+ interface Order {
11
+ id: string; code: string; state: string; total: number; orderPlacedAt: string | null;
12
+ lines: OrderLine[];
13
+ fulfillments: Array<{ state: string; method: string; trackingCode: string | null }>;
14
+ }
15
+
16
+ interface ChatMessage {
17
+ id: string;
18
+ role: 'user' | 'assistant';
19
+ content: string;
20
+ products?: Product[];
21
+ collections?: Collection[];
22
+ orders?: Order[];
23
+ }
24
+
25
+ // ─── Component ──────────────────────────────────────────────────────
26
+
27
+ export function AdminChatWidget() {
28
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
29
+ const [input, setInput] = useState('');
30
+ const [isLoading, setIsLoading] = useState(false);
31
+ const scrollRef = useRef<HTMLDivElement>(null);
32
+
33
+ useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
34
+
35
+ function getHistory(): Array<{ role: 'user' | 'assistant'; content: string }> {
36
+ return messages.map(m => ({ role: m.role, content: m.content }));
37
+ }
38
+
39
+ async function sendMessage(text: string) {
40
+ if (!text.trim() || isLoading) return;
41
+ setMessages(prev => [...prev, { id: `u-${Date.now()}`, role: 'user', content: text.trim() }]);
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;
50
+
51
+ const res = await fetch('/admin-ai-chat/chat', {
52
+ method: 'POST', headers, credentials: 'include',
53
+ body: JSON.stringify({ message: text.trim(), history: getHistory() }),
54
+ });
55
+ const data = await res.json();
56
+ setMessages(prev => [...prev, {
57
+ id: `a-${Date.now()}`, role: 'assistant', content: data.message ?? 'No response',
58
+ products: data.products, collections: data.collections, orders: data.orders,
59
+ }]);
60
+ } catch (err) {
61
+ setMessages(prev => [...prev, { id: `e-${Date.now()}`, role: 'assistant', content: `Error: ${err instanceof Error ? err.message : 'Request failed'}` }]);
62
+ } finally { setIsLoading(false); }
63
+ }
64
+
65
+ return (
66
+ <div className="ac-root">
67
+ <div className="ac-messages">
68
+ {messages.length === 0 && (
69
+ <div className="ac-empty">
70
+ <div className="ac-empty-icon">
71
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
72
+ <path d="M12 6V2H8"/><path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z"/>
73
+ <path d="M2 12h2"/><path d="M9 11v2"/><path d="M15 11v2"/><path d="M20 12h2"/>
74
+ </svg>
75
+ </div>
76
+ <div className="ac-empty-title">Admin AI Assistant</div>
77
+ <div className="ac-empty-sub">Ask about products, orders, customers, or collections</div>
78
+ <div className="ac-chips">
79
+ {['Show recent orders', 'Search for laptops', 'List collections', 'Find customers named john'].map(a => (
80
+ <button key={a} className="ac-chip" onClick={() => sendMessage(a)}>{a}</button>
81
+ ))}
82
+ </div>
83
+ </div>
84
+ )}
85
+
86
+ {messages.map(msg => (
87
+ <div key={msg.id} className={`ac-row ac-row--${msg.role}`}>
88
+ {msg.role === 'assistant' && <div className="ac-avatar">AI</div>}
89
+ <div className="ac-msg-col">
90
+ <div className={`ac-bubble ac-bubble--${msg.role}`}>
91
+ {msg.role === 'assistant'
92
+ ? <div className="ac-md"><Markdown remarkPlugins={[remarkGfm]}>{msg.content}</Markdown></div>
93
+ : msg.content}
94
+ </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
+ </div>
108
+ </div>
109
+ ))}
110
+
111
+ {isLoading && (
112
+ <div className="ac-row ac-row--assistant">
113
+ <div className="ac-avatar">AI</div>
114
+ <div className="ac-bubble ac-bubble--assistant"><span className="ac-dots"><span/><span/><span/></span></div>
115
+ </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>
150
+ </div>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ function OrderCard({ order }: { order: Order }) {
156
+ const total = `$${(order.total / 100).toFixed(2)}`;
157
+ const date = order.orderPlacedAt ? new Date(order.orderPlacedAt).toLocaleDateString() : 'N/A';
158
+ const stateColor = order.state === 'PaymentSettled' ? '#22c55e' : order.state === 'Cancelled' ? '#ef4444' : '#6366f1';
159
+ return (
160
+ <div className="ac-ocard" onClick={() => window.open(`/dashboard/orders/${order.id}`, '_self')}>
161
+ <div className="ac-ocard-header">
162
+ <span className="ac-ocard-code">{order.code}</span>
163
+ <span className="ac-ocard-state" style={{ background: stateColor }}>{order.state}</span>
164
+ </div>
165
+ <div className="ac-ocard-meta">{date} &middot; {total} &middot; {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
+ )}
177
+ </div>
178
+ );
179
+ }
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
+ `;
@@ -0,0 +1,55 @@
1
+ import { defineDashboardExtension } from '@vendure/dashboard';
2
+ import { AdminChatWidget } from './AdminChatWidget';
3
+ import React from 'react';
4
+
5
+ function AiChatPage() {
6
+ return (
7
+ <div style={{
8
+ display: 'flex',
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>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ defineDashboardExtension({
36
+ routes: [
37
+ {
38
+ path: '/extensions/ai-chat',
39
+ component: () => <AiChatPage />,
40
+ navMenuItem: {
41
+ id: 'ai-chat',
42
+ title: 'AI Chat',
43
+ sectionId: 'ai',
44
+ },
45
+ },
46
+ ],
47
+ navSections: [
48
+ {
49
+ id: 'ai',
50
+ title: 'AI',
51
+ placement: 'top',
52
+ order: 150,
53
+ },
54
+ ],
55
+ });