Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ absl
achatassistant
ACMRTUXB
Adyen
agentic
Agentic
agentic
agenticpayments
Algorand
algovoi
androidx
Applebot
appname
Expand All @@ -15,6 +16,11 @@ bazel
Blackhawk
Boku
BVNK
canonicalisation
Canonicalisation
canonicalise
canonicalised
canonicalising
celerybeat
classpath
CLASSPATH
Expand All @@ -29,14 +35,17 @@ Crossmint
cryptographical
CYGPATTERN
Dafiti
disclosable
Disclosable
davecgh
dcql
Dcql
DCQL
dcql
deserialisation
deserialise
deserialising
deviceauth
Dfile
Disclosable
disclosable
dmypy
Doku
Dorg
Expand Down Expand Up @@ -68,6 +77,7 @@ groupcache
gson
Hashkey
honnef
hopley
hprof
htmlcov
httpsnoop
Expand All @@ -87,10 +97,10 @@ keepattributes
keepclassmembers
Klarna
kotlin
kotlinx
Kotlinx
ktor
kotlinx
Ktor
ktor
KXMYBJWNQ
Lazada
libpeerconnection
Expand All @@ -107,9 +117,11 @@ Momo
Monee
msys
MSYS
multiparty
multistep
Mysten
nexi
normalised
nosetests
Nuvei
objx
Expand All @@ -126,10 +138,13 @@ Paynet
Payoneer
paypal
Payplug
pef
PEF
pids
pmezard
proguard
preimage
Proguard
proguard
prometheus
protoc
pyflow
Expand All @@ -149,6 +164,8 @@ ropeproject
RPCURL
Rulebook
screenreaders
serialised
serialising
setlocal
sharedpref
Shopcider
Expand All @@ -163,12 +180,17 @@ spyderproject
spyproject
stablecoins
stdr
sublabel
dedup
Sublabel
stretchr
superfences
Truelayer
Trulioo
udpa
unmarshal
unrecognised
verdicts
viewmodel
vulnz
Wallex
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,13 @@ jobs:
VALIDATE_TRIVY: false
VALIDATE_PYTHON_RUFF: false
VALIDATE_PYTHON_RUFF_FORMAT: false
# The project uses Biome for TypeScript/TSX linting (BIOME_LINT above).
# Super-linter warns that running both Biome and ESLint on the same files
# causes conflicts; disable the ESLint-based TS/TSX linters so Biome is
# the single source of truth for TypeScript quality.
# ESLint false-positives also appear because:
# - TSX: super-linter ESLint lacks node_modules so all imports fail
# - TSX: react/react-in-jsx-scope is obsolete for React 17+ JSX transform
# - TYPESCRIPT_ES: browser APIs (fetch, crypto) flagged as unsupported Node builtins
VALIDATE_TSX: false
VALIDATE_TYPESCRIPT_ES: false
91 changes: 52 additions & 39 deletions code/web-client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
import {useEffect, useRef, useState} from 'react';
import './App.scss';
import {MandateViewer} from './components/MandateViewer';
import {MessageRenderer} from './components/MessageRenderer';
import {TypingIndicator} from './components/TypingIndicator';
import {DEFAULT_CHAT_STARTER_MESSAGE} from './config';
import {type ChatState, useChat} from './hooks/useChat';
import { useEffect, useRef, useState } from "react";
import "./App.scss";
import { MandateViewer } from "./components/MandateViewer";
import { MessageRenderer } from "./components/MessageRenderer";
import { TypingIndicator } from "./components/TypingIndicator";
import { DEFAULT_CHAT_STARTER_MESSAGE } from "./config";
import { type ChatState, useChat } from "./hooks/useChat";

// ==========================================
// SUB-COMPONENTS
// ==========================================

const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
const AppHeader = ({ usedServers }: { usedServers: Set<string> }) => {
const servers = [
{
label: 'Shopping Agent',
key: 'Shopping Agent',
className: 'server-shopping',
label: "Shopping Agent",
key: "Shopping Agent",
className: "server-shopping",
},
{label: 'Merchant MCP', key: 'Merchant MCP', className: 'server-merchant'},
{
label: 'Credential Provider MCP',
key: 'Credential Provider MCP',
className: 'server-credential',
label: "Merchant MCP",
key: "Merchant MCP",
className: "server-merchant",
},
{
label: "Credential Provider MCP",
key: "Credential Provider MCP",
className: "server-credential",
},
];

const flow = (import.meta as any).env?.VITE_FLOW;
const flow = (import.meta as { env?: { VITE_FLOW?: string } }).env?.VITE_FLOW;

return (
<div className="app-header">
Expand All @@ -35,8 +39,8 @@ const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
<div className="title-container">
<div className="title">
Delegated Shopper
{flow === 'x402' && <span className="flow-badge x402">x402</span>}
{flow === 'card' && <span className="flow-badge card">Card</span>}
{flow === "x402" && <span className="flow-badge x402">x402</span>}
{flow === "card" && <span className="flow-badge card">Card</span>}
</div>
<div className="subtitle">
A2A · Human-not-present · Merchant MCP · Credential Provider MCP
Expand All @@ -46,7 +50,7 @@ const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
{servers.map((b) => (
<div
key={b.key}
className={`server-badge ${usedServers.has(b.key) ? 'active' : ''} ${b.className}`}>
className={`server-badge ${usedServers.has(b.key) ? "active" : ""} ${b.className}`}>
<div className="dot" />
<span className="label">{b.label}</span>
</div>
Expand All @@ -56,7 +60,7 @@ const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
);
};

type TabKey = 'chat' | 'mandates';
type TabKey = "chat" | "mandates";

const TabBar = ({
activeTab,
Expand All @@ -69,13 +73,15 @@ const TabBar = ({
}) => (
<div className="tab-bar">
<button
className={`tab ${activeTab === 'chat' ? 'active' : ''}`}
onClick={() => onChange('chat')}>
type="button"
className={`tab ${activeTab === "chat" ? "active" : ""}`}
onClick={() => onChange("chat")}>
Chat
</button>
<button
className={`tab ${activeTab === 'mandates' ? 'active' : ''}`}
onClick={() => onChange('mandates')}>
type="button"
className={`tab ${activeTab === "mandates" ? "active" : ""}`}
onClick={() => onChange("mandates")}>
Mandates
{mandateCount > 0 && <span className="tab-count">{mandateCount}</span>}
</button>
Expand All @@ -93,10 +99,10 @@ const EmptyChatState = () => (
via Merchant MCP + Credential Provider MCP
</div>
<p className="suggestion">
Try:{' '}
Try:{" "}
<em>
&quot;When is the SuperShoe limited edition Gold sneaker drop? I need size 9
women&apos;s.&quot;
&quot;When is the SuperShoe limited edition Gold sneaker drop? I need
size 9 women&apos;s.&quot;
</em>
</p>
<p className="suggestion-enter-hint">
Expand All @@ -107,26 +113,32 @@ const EmptyChatState = () => (

type ChatInputProps = Pick<
ChatState,
'handleSend' | 'input' | 'loading' | 'setInput'
"handleSend" | "input" | "loading" | "setInput"
>;

const ChatInput = ({input, setInput, handleSend, loading}: ChatInputProps) => (
const ChatInput = ({
input,
setInput,
handleSend,
loading,
}: ChatInputProps) => (
<div className="input-area">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) =>
e.key === 'Enter' &&
e.key === "Enter" &&
!loading &&
handleSend({fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE})
handleSend({ fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE })
}
placeholder="e.g. When is the SuperShoe limited edition Gold sneaker drop? I need size 9 women's."
disabled={loading}
className="chat-input"
/>
<button
type="button"
onClick={() =>
handleSend({fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE})
handleSend({ fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE })
}
disabled={loading}
className="send-button">
Expand All @@ -141,14 +153,15 @@ const ChatInput = ({input, setInput, handleSend, loading}: ChatInputProps) => (

export default function App() {
const chatState: ChatState = useChat();
const [activeTab, setActiveTab] = useState<TabKey>('chat');
const { messages } = chatState;
const [activeTab, setActiveTab] = useState<TabKey>("chat");
const bottomRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (activeTab === 'chat') {
bottomRef.current?.scrollIntoView({behavior: 'smooth'});
if (activeTab === "chat" && messages.length > 0) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [chatState.messages, activeTab]);
}, [messages, activeTab]);

return (
<div className="app-container">
Expand All @@ -159,12 +172,12 @@ export default function App() {
mandateCount={chatState.mandates.length}
/>

{activeTab === 'chat' ? (
{activeTab === "chat" ? (
<>
<div className="messages-container">
{chatState.messages.length > 0 ? (
{messages.length > 0 ? (
<div className="messages-list">
{chatState.messages.map((msg) => (
{messages.map((msg) => (
<MessageRenderer
key={msg.id}
msg={msg}
Expand Down
18 changes: 11 additions & 7 deletions code/web-client/src/components/InventoryOptionsCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(52, 211, 153, 0.15);
border: 1px solid rgba(52, 211, 153, 0.3);
background: rgb(52 211 153 / 15%);
border: 1px solid rgb(52 211 153 / 30%);
display: flex;
align-items: center;
justify-content: center;
}

.tool-label {
font-family: 'IBM Plex Mono', monospace;
font-family: "IBM Plex Mono", monospace;
font-size: 11px;
color: #34d399;
}
Expand Down Expand Up @@ -47,7 +47,7 @@

.selected-item-id {
color: #93c5fd;
font-family: 'IBM Plex Mono', monospace;
font-family: "IBM Plex Mono", monospace;
font-size: 11px;
}
}
Expand All @@ -59,7 +59,7 @@
border: none;
padding: 10px 16px;
border-radius: 9px;
font-family: 'Geist', sans-serif;
font-family: Geist, sans-serif;
font-size: 13px;
font-weight: 600;
cursor: pointer;
Expand Down Expand Up @@ -121,13 +121,15 @@

.item-details {
.item-name {
font-family: 'Geist', sans-serif;
display: block;
font-family: Geist, sans-serif;
font-size: 13px;
font-weight: 500;
color: #e2e8f0;
}

.item-id {
display: block;
font-size: 11px;
color: #6b7280;
margin-top: 1px;
Expand All @@ -140,13 +142,15 @@
margin-left: 12px;

.item-price {
font-family: 'IBM Plex Mono', monospace;
display: block;
font-family: "IBM Plex Mono", monospace;
font-size: 14px;
font-weight: 500;
color: #e2e8f0;
}

.item-stock {
display: block;
font-size: 10px;
color: #4b5563;
margin-top: 1px;
Expand Down
Loading
Loading