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
3 changes: 3 additions & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ spyderproject
spyproject
stablecoins
stdr
sublabel
dedup
Sublabel
stretchr
superfences
Truelayer
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