@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 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
@@ -125,4 +125,5 @@ This is a **dashboard-only** package — it contains no backend logic. The archi
125
125
 
126
126
  ## License
127
127
 
128
- MIT
128
+
129
+ AGPL-3.0 — see [LICENSE](./LICENSE) for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahul_vendure/ai-chat-dashboard",
3
- "version": "0.1.2",
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": "MIT",
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, { 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[];
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 [messages, setMessages] = useState<ChatMessage[]>([]);
29
- const [input, setInput] = useState('');
30
- const [isLoading, setIsLoading] = useState(false);
31
- const scrollRef = useRef<HTMLDivElement>(null);
34
+ const {
35
+ messages, input, setInput, isLoading, scrollRef,
36
+ sendMessage, sessionId, newConversation, loadSession,
37
+ } = useAdminChat();
32
38
 
33
- useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
39
+ const { sessions, refresh: refreshSessions, deleteSession } = useSessionList();
34
40
 
35
- function getHistory(): Array<{ role: 'user' | 'assistant'; content: string }> {
36
- return messages.map(m => ({ role: m.role, content: m.content }));
37
- }
41
+ const handleNewChat = useCallback(() => {
42
+ newConversation();
43
+ }, [newConversation]);
38
44
 
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;
45
+ const handleSelectSession = useCallback(async (sid: string) => {
46
+ await loadSession(sid);
47
+ }, [loadSession]);
50
48
 
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
- }
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 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
- )}
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
- {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}
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
- </div>
109
- ))}
104
+ )}
105
+ </div>
110
106
 
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>
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
- 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
- )}
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
- `;