Skip to content

Commit 96fa0fc

Browse files
committed
feat: add Agentic UI components for enhanced interactive documentation experience
1 parent 5734289 commit 96fa0fc

3 files changed

Lines changed: 634 additions & 2 deletions

File tree

components/AgenticUI.jsx

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// AgenticUI.jsx
2+
// ---------------------------------------------------------------------------
3+
// Agentic UI components rendered inline in the CopilotKit chat panel.
4+
// These are used via the `render` and `renderAndWaitForResponse` properties
5+
// on useCopilotAction hooks.
6+
// ---------------------------------------------------------------------------
7+
import React from "react";
8+
9+
const DOCS = "https://dspreadorg.github.io/docs";
10+
11+
// ── Route → display metadata ────────────────────────────────────────────
12+
const PAGE_META = {
13+
"/": { icon: "🏠", label: "Overview" },
14+
"/plan-your-integration": { icon: "📋", label: "Plan Your Integration" },
15+
"/how-terminal-works": { icon: "⚙️", label: "How Terminal Works" },
16+
"/android-terminals/overview": { icon: "📱", label: "Android Overview" },
17+
"/android-terminals/set-up-integration": { icon: "🔧", label: "Set Up Integration" },
18+
"/android-terminals/accept-card-payment": { icon: "💳", label: "Accept Card Payment" },
19+
"/android-terminals/print-receipt": { icon: "🖨️", label: "Print Receipt" },
20+
"/android-terminals/scanner-qr-bar-code": { icon: "📷", label: "Scanner QR/Bar Code" },
21+
"/android-terminals/customize-os": { icon: "🎨", label: "Customize OS" },
22+
"/linux-terminals/getting-started": { icon: "🐧", label: "Linux Getting Started" },
23+
"/linux-terminals/transaction-flow": { icon: "🔄", label: "Transaction Flow" },
24+
"/linux-terminals/best-practices": { icon: "✅", label: "Best Practices" },
25+
"/linux-terminals/common-issues": { icon: "🐛", label: "Common Issues" },
26+
"/linux-terminals/additional-resources": { icon: "📚", label: "Additional Resources" },
27+
"/cloud-speaker": { icon: "🔊", label: "Cloud Speaker" },
28+
"/key-management-aws": { icon: "🔐", label: "Key Management (AWS)" },
29+
"/payment-gateway-aws": { icon: "🌐", label: "Payment Gateway (AWS)" },
30+
"/emv-l3-testing": { icon: "🧪", label: "EMV L3 Testing" },
31+
"/tms-larktms": { icon: "📡", label: "TMS / LarkTMS" },
32+
};
33+
34+
// ── Product category metadata ───────────────────────────────────────────
35+
const PRODUCT_CATEGORIES = [
36+
{
37+
id: "smartpos",
38+
label: "Smart POS (Android)",
39+
icon: "📱",
40+
models: "D20, D30, D50, D60, D70, D80, D80K",
41+
color: "#3b82f6",
42+
description: "Full Android POS terminals with built-in apps",
43+
},
44+
{
45+
id: "mpos",
46+
label: "mPOS / Mobile Reader",
47+
icon: "📲",
48+
models: "QPOS mini, QPOS Cute, CR100, QPOS Plus",
49+
color: "#8b5cf6",
50+
description: "Bluetooth/USB readers paired with phone",
51+
},
52+
{
53+
id: "linux",
54+
label: "Linux Terminal",
55+
icon: "🐧",
56+
models: "D30-linux, QPOS-linux",
57+
color: "#10b981",
58+
description: "Linux-based POS with C/C++ SDK",
59+
},
60+
{
61+
id: "cloud-speaker",
62+
label: "Cloud Speaker",
63+
icon: "🔊",
64+
models: "DS10, DS50, DS200",
65+
color: "#f59e0b",
66+
description: "Audio payment notification devices",
67+
},
68+
];
69+
70+
// ═══════════════════════════════════════════════════════════════════════
71+
// NavigationCard — rendered inline when AI navigates to a docs page
72+
// ═══════════════════════════════════════════════════════════════════════
73+
export function NavigationCard({ route, pageTitle, status }) {
74+
const meta = PAGE_META[route] || { icon: "📄", label: pageTitle || route };
75+
const isComplete = status === "complete";
76+
const fullUrl = `${DOCS}${route}`;
77+
78+
return (
79+
<div className={`agentic-card agentic-nav-card ${isComplete ? "agentic-card-complete" : "agentic-card-executing"}`}>
80+
<div className="agentic-card-header">
81+
<span className="agentic-card-icon">{meta.icon}</span>
82+
<span className="agentic-card-badge">
83+
{isComplete ? "✓ Navigated" : "⏳ Navigating…"}
84+
</span>
85+
</div>
86+
<div className="agentic-card-title">{meta.label}</div>
87+
<div className="agentic-card-route">{route}</div>
88+
{isComplete && (
89+
<a
90+
href={fullUrl}
91+
target="_blank"
92+
rel="noopener noreferrer"
93+
className="agentic-card-link"
94+
>
95+
Open in new tab →
96+
</a>
97+
)}
98+
</div>
99+
);
100+
}
101+
102+
// ═══════════════════════════════════════════════════════════════════════
103+
// ProductSelector — interactive product type picker (renderAndWaitForResponse)
104+
// ═══════════════════════════════════════════════════════════════════════
105+
export function ProductSelector({ respond, status }) {
106+
if (status === "complete") {
107+
return (
108+
<div className="agentic-card agentic-card-complete">
109+
<div className="agentic-card-header">
110+
<span className="agentic-card-icon"></span>
111+
<span className="agentic-card-badge">Product Selected</span>
112+
</div>
113+
</div>
114+
);
115+
}
116+
117+
return (
118+
<div className="agentic-card agentic-product-selector">
119+
<div className="agentic-card-header">
120+
<span className="agentic-card-icon">🎯</span>
121+
<span className="agentic-card-title" style={{ marginLeft: 8 }}>
122+
Select your product type
123+
</span>
124+
</div>
125+
<p className="agentic-card-subtitle">
126+
Click a product to get targeted documentation:
127+
</p>
128+
<div className="agentic-product-grid">
129+
{PRODUCT_CATEGORIES.map((cat) => (
130+
<button
131+
key={cat.id}
132+
className="agentic-product-btn"
133+
style={{ "--product-color": cat.color }}
134+
onClick={() =>
135+
respond?.(
136+
`I'm using a ${cat.label} terminal. Models: ${cat.models}`
137+
)
138+
}
139+
>
140+
<span className="agentic-product-icon">{cat.icon}</span>
141+
<span className="agentic-product-label">{cat.label}</span>
142+
<span className="agentic-product-models">{cat.models}</span>
143+
<span className="agentic-product-desc">{cat.description}</span>
144+
</button>
145+
))}
146+
</div>
147+
</div>
148+
);
149+
}
150+
151+
// ═══════════════════════════════════════════════════════════════════════
152+
// RelatedPagesCard — displays a grid of related documentation pages
153+
// ═══════════════════════════════════════════════════════════════════════
154+
export function RelatedPagesCard({ pages, status, onNavigate }) {
155+
const isComplete = status === "complete";
156+
157+
// pages is an array of { route, title }
158+
const displayPages = (pages || []).slice(0, 6);
159+
160+
if (!displayPages.length) {
161+
return null;
162+
}
163+
164+
return (
165+
<div className={`agentic-card agentic-related-card ${isComplete ? "agentic-card-complete" : "agentic-card-executing"}`}>
166+
<div className="agentic-card-header">
167+
<span className="agentic-card-icon">📚</span>
168+
<span className="agentic-card-title" style={{ marginLeft: 8 }}>
169+
Related Documentation
170+
</span>
171+
</div>
172+
<div className="agentic-pages-grid">
173+
{displayPages.map((page, i) => {
174+
const meta = PAGE_META[page.route] || { icon: "📄", label: page.title };
175+
return (
176+
<button
177+
key={i}
178+
className="agentic-page-btn"
179+
onClick={() => onNavigate?.(page.route)}
180+
>
181+
<span className="agentic-page-icon">{meta.icon}</span>
182+
<span className="agentic-page-label">{meta.label}</span>
183+
<span className="agentic-page-route">{page.route}</span>
184+
</button>
185+
);
186+
})}
187+
</div>
188+
</div>
189+
);
190+
}
191+
192+
// ═══════════════════════════════════════════════════════════════════════
193+
// ProgressCard — shows step-by-step progress during multi-step actions
194+
// ═══════════════════════════════════════════════════════════════════════
195+
export function ProgressCard({ title, steps, currentStep, status }) {
196+
return (
197+
<div className={`agentic-card agentic-progress-card ${status === "complete" ? "agentic-card-complete" : "agentic-card-executing"}`}>
198+
<div className="agentic-card-header">
199+
<span className="agentic-card-icon">
200+
{status === "complete" ? "✅" : "⚡"}
201+
</span>
202+
<span className="agentic-card-title" style={{ marginLeft: 8 }}>
203+
{title}
204+
</span>
205+
</div>
206+
<div className="agentic-steps">
207+
{(steps || []).map((step, i) => {
208+
const isDone = i < currentStep;
209+
const isCurrent = i === currentStep && status !== "complete";
210+
return (
211+
<div
212+
key={i}
213+
className={`agentic-step ${isDone ? "agentic-step-done" : ""} ${isCurrent ? "agentic-step-active" : ""}`}
214+
>
215+
<span className="agentic-step-indicator">
216+
{isDone ? "✓" : isCurrent ? "●" : "○"}
217+
</span>
218+
<span className="agentic-step-text">{step}</span>
219+
</div>
220+
);
221+
})}
222+
</div>
223+
</div>
224+
);
225+
}

components/CopilotKitWrapper.jsx

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ import {
1515
} from "@copilotkit/react-ui";
1616
import "@copilotkit/react-ui/styles.css";
1717
import { useRouter } from "next/router";
18-
import { useEffect, useMemo, useState } from "react";
18+
import { useCallback, useEffect, useMemo, useState } from "react";
1919
import { fetchAllGithubContent } from "../utils/fetchGithubContent";
20+
import {
21+
NavigationCard,
22+
ProductSelector,
23+
RelatedPagesCard,
24+
ProgressCard,
25+
} from "./AgenticUI";
2026

2127
const DOCS = "https://dspreadorg.github.io/docs";
2228

@@ -79,7 +85,18 @@ Once the product type is known:
7985
- **Cloud Speaker**: Guide through build process. Direct to Cloud Speaker page.
8086
8187
### Step 3 — Always use the navigateToPage action
82-
When your answer relates to a specific documentation page, call the **navigateToPage** action to navigate the user's browser to that page. This is MANDATORY — do not just provide links, actively navigate.
88+
When your answer relates to a specific documentation page, call the **navigateToPage** action to navigate the user's browser to that page. This is MANDATORY — do not just provide links, actively navigate. A styled navigation card will appear in the chat.
89+
90+
### Step 4 — Use Agentic UI actions for better UX
91+
You have several visual actions that render interactive cards in the chat:
92+
93+
1. **selectProductType** — At the START of a conversation, if the user's product type is unknown, call this action. It shows an interactive product selector card with buttons. Wait for the user's choice before proceeding.
94+
95+
2. **showRelatedPages** — At the END of your answer, call this to show a card with clickable links to related documentation pages. Always include 2-4 relevant pages.
96+
97+
3. **showIntegrationGuide** — When walking a user through a multi-step process (SDK setup, payment flow, certification), call this to show a numbered step-by-step card. Update the currentStep index as you progress.
98+
99+
CRITICAL: These actions render visual UI IN the chat. Use them proactively to create a rich, interactive experience. Don't just use text when a card would be more helpful.
83100
84101
## PAGE ROUTE MAP (use these exact routes with navigateToPage):
85102
- Overview: /
@@ -262,7 +279,19 @@ Key differences:
262279
- Cloud Speaker is audio notification device with custom firmware`,
263280
});
264281

282+
// ─ helper for child actions that need to navigate ─────────────────────
283+
const handleNavigate = useCallback(
284+
(route) => {
285+
const normalized = (route || "").replace(/\/+$/, "") || "/";
286+
if (VALID_ROUTES.includes(normalized)) {
287+
router.push(normalized);
288+
}
289+
},
290+
[router],
291+
);
292+
265293
// ─ Navigation action — AI calls this to jump to a docs page ──────────
294+
// Now with AGENTIC UI: renders a styled card inline in the chat.
266295
useCopilotAction({
267296
name: "navigateToPage",
268297
description:
@@ -303,6 +332,111 @@ Key differences:
303332
router.push(normalized);
304333
return `Navigated to "${pageTitle}" (${normalized})`;
305334
},
335+
// ── Agentic UI: show navigation card in chat ──
336+
render: ({ args, status }) => (
337+
<NavigationCard
338+
route={args?.route || "/"}
339+
pageTitle={args?.pageTitle || ""}
340+
status={status}
341+
/>
342+
),
343+
});
344+
345+
// ─ Product selector action — interactive product type picker ──────────
346+
// Uses renderAndWaitForResponse: the AI pauses while user picks a product.
347+
useCopilotAction({
348+
name: "selectProductType",
349+
description:
350+
"Show an interactive product type selector card. Call this when you need " +
351+
"the user to choose their product type (Smart POS, mPOS, Linux, Cloud Speaker) " +
352+
"and you want to present a visual picker instead of asking via text. " +
353+
"ONLY call this once at the start of a conversation when the product type is unknown.",
354+
parameters: [],
355+
renderAndWaitForResponse: ({ respond, status }) => (
356+
<ProductSelector respond={respond} status={status} />
357+
),
358+
});
359+
360+
// ─ Show related pages action — displays a grid of related pages ───────
361+
useCopilotAction({
362+
name: "showRelatedPages",
363+
description:
364+
"Show a card with related documentation pages. Call this when you want to " +
365+
"recommend multiple pages to the user, for example at the end of an answer. " +
366+
"Pass an array of pages with route and title.",
367+
parameters: [
368+
{
369+
name: "pages",
370+
type: "object[]",
371+
description: "Array of related pages to display",
372+
attributes: [
373+
{
374+
name: "route",
375+
type: "string",
376+
description: "Page route (must be a valid route from PAGE ROUTE MAP)",
377+
required: true,
378+
},
379+
{
380+
name: "title",
381+
type: "string",
382+
description: "Human-readable page title",
383+
required: true,
384+
},
385+
],
386+
required: true,
387+
},
388+
],
389+
handler: ({ pages }) => {
390+
return `Showing ${(pages || []).length} related pages.`;
391+
},
392+
render: ({ args, status }) => (
393+
<RelatedPagesCard
394+
pages={args?.pages || []}
395+
status={status}
396+
onNavigate={handleNavigate}
397+
/>
398+
),
399+
});
400+
401+
// ─ Progress/guide action — shows step-by-step integration guide ───────
402+
useCopilotAction({
403+
name: "showIntegrationGuide",
404+
description:
405+
"Show a step-by-step progress card for integration guidance. " +
406+
"Use this when walking a user through a multi-step process like " +
407+
"SDK setup, payment integration, or certification. " +
408+
"Provide the list of steps and the current step index (0-based).",
409+
parameters: [
410+
{
411+
name: "title",
412+
type: "string",
413+
description: "Title of the integration guide. E.g. 'Android SDK Setup'",
414+
required: true,
415+
},
416+
{
417+
name: "steps",
418+
type: "string[]",
419+
description: "List of step descriptions in order.",
420+
required: true,
421+
},
422+
{
423+
name: "currentStep",
424+
type: "number",
425+
description: "The index of the current step being worked on (0-based).",
426+
required: true,
427+
},
428+
],
429+
handler: ({ title, steps, currentStep }) => {
430+
return `Integration guide: "${title}" — step ${currentStep + 1} of ${(steps || []).length}`;
431+
},
432+
render: ({ args, status }) => (
433+
<ProgressCard
434+
title={args?.title || "Guide"}
435+
steps={args?.steps || []}
436+
currentStep={args?.currentStep ?? 0}
437+
status={status}
438+
/>
439+
),
306440
});
307441

308442
// ─ Dynamic suggestion instructions based on current page ──────────────

0 commit comments

Comments
 (0)