diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..fab90d9 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,168 @@ +--- +name: AxiomID Agentic +colors: + primary: "#00FF41" + secondary: "#00D4FF" + accent: "#FBBF24" + success: "#16A34A" + warning: "#D97706" + danger: "#DC2626" + background: "#060608" + surface: "#09090B" + foreground: "#F4F4F6" + muted: "#121217" + border: "rgba(0, 255, 65, 0.08)" +typography: + h1: + fontFamily: "Geist" + fontSize: 2.5rem + h2: + fontFamily: "Geist" + fontSize: 1.75rem + body-md: + fontFamily: "Geist" + fontSize: 1rem + body-sm: + fontFamily: "Geist" + fontSize: 0.875rem + label-caps: + fontFamily: "Geist Mono" + fontSize: 0.75rem + textTransform: uppercase + letterSpacing: 0.05em + sourceScale: "14/16/18/24/32/40" + weights: "300, 400, 500, 600, 700, 800, 900" +rounded: + sm: 8px + md: 14px + lg: 20px + full: 9999px +spacing: + xs: 4px + sm: 8px + md: 16px + lg: 24px + xl: 32px + sourceScale: "8pt baseline grid" +--- + +## Overview + +Conversational AI-first interface with minimal controls, clear outcomes, and delegated task flows for agentic workflows. Dark cyberpunk aesthetic with neon green accents and glass-morphism surfaces. + +## Style Foundations + +- **Visual style:** dark, modern, cyberpunk, glass-morphism +- **Typography scale:** 14/16/18/24/32/40 +- **Typography fonts:** primary=Geist, display=Geist, mono=Geist Mono +- **Typography weights:** 300, 400, 500, 600, 700, 800, 900 +- **Color palette:** dark background (#060608), neon green primary (#00FF41), electric blue secondary (#00D4FF), gold accent (#FBBF24) +- **Spacing scale:** 8pt baseline grid +- **Surfaces:** glass-panel with backdrop-filter blur(12px), semi-transparent borders +- **Glow effects:** text-shadow and box-shadow with rgba primary color glow + +## Colors + +- **Background (#060608):** Deep near-black base. +- **Surface (#09090B):** Slightly lighter card/panel surfaces. +- **Foreground (#F4F4F6):** Near-white text on dark backgrounds. +- **Primary (#00FF41):** Neon green — interactive elements, active states, hover glows. +- **Secondary (#00D4FF):** Electric blue — secondary actions, gradients. +- **Accent (#FBBF24):** Gold — highlights, badges, premium indicators. +- **Success (#16A34A):** Green status indicators. +- **Warning (#D97706):** Amber warning signals. +- **Danger (#DC2626):** Red destructive actions and error states. +- **Muted (#121217):** Subtle background layers, dividers. +- **Border (rgba(0, 255, 65, 0.08)):** Subtle green-tinted borders. + +## Typography + +- **Headings:** Geist (sans-serif), bold weights (600-800) +- **Body:** Geist (sans-serif), weight 400 +- **Mono:** Geist Mono for code, labels, and data displays +- **Label caps:** 0.75rem, uppercase, 0.05em letter-spacing, Geist Mono + +## Spacing + +- **Grid:** 8pt baseline grid +- **Card padding:** 20-24px +- **Section gaps:** 32-48px +- **Component gaps:** 12-16px + +## Rounded Corners + +- **Cards (bento):** 20px radius with glass border +- **Buttons:** 12px radius +- **Inputs:** 10px radius +- **Pills/badges:** 9999px (full rounded) + +## Components + +### Bento Card +- Background: rgba(9, 9, 11, 0.75) with backdrop-filter blur(16px) +- Border: 1px solid rgba(0, 255, 65, 0.08) +- Border-radius: 20px +- Top gradient line on hover: linear-gradient(90deg, transparent, #00FF41, #00D4FF, transparent) +- Hover: translateY(-2px), green glow shadow, border opacity increase + +### Primary Button +- Gradient background: rgba(0, 255, 65, 0.15) to rgba(0, 212, 255, 0.15) +- Hover: solid #00FF41 to #00D4FF gradient, black text +- Text: 0.75rem uppercase, Geist Mono, 0.05em letter-spacing + +### Ghost Button +- Background: rgba(255, 255, 255, 0.02) +- Border: 1px solid rgba(255, 255, 255, 0.05) +- Hover: white text, brighter border + +### Engineering Grid Background +- Background pattern: 40px grid with subtle white lines at 1.5% opacity +- Mask: radial gradient at center (black 30%, transparent 95%) +- Scanline: animated 4px top-to-bottom purple gradient line + +### Glow Text +- .text-neon: #00FF41 with 10px green text-shadow +- .text-electric: #00D4FF with 10px blue text-shadow + +### Scrollbar +- Width: 5px +- Track: #030305 +- Thumb: #1F1F2E, rounded, hover turns green (#00FF41) + +## Animation + +- Card hover: 0.5s cubic-bezier(0.16, 1, 0.3, 1) +- Button hover: 0.3s cubic-bezier(0.16, 1, 0.3, 1) +- Scanline: 8s linear infinite +- Pulse-slow: 3s cubic-bezier(0.4, 0, 0.6, 1) infinite + +## Accessibility + +- Contrast ratio minimum 4.5:1 for text +- Focus indicators with green glow outline +- Interactive elements have visible hover/focus states +- Reduced motion: respect prefers-reduced-motion + +## Writing Tone + +- Technical, concise, Arabic-supportive +- English UI labels, Arabic content where appropriate +- Short labels, clear CTAs + +## Rules: Do + +- Use dark theme as default (color-scheme: dark) +- Use neon green (#00FF41) for primary CTAs and active states +- Use glass-morphism for cards and panels +- Apply backdrop-filter blur on overlay surfaces +- Use Geist Mono for code, labels, technical data +- Use gradient borders on hover for interactive cards +- Keep generous whitespace and breathing room + +## Rules: Don't + +- Don't use pure white backgrounds (#FFFFFF) on dark surfaces +- Don't use bright saturated colors without alpha for backgrounds +- Don't add box-shadows without considering glow effects +- Don't use non-Geist font families +- Don't create heavy/dense layouts — prefer bento grid with whitespace diff --git a/apps/axiomid/.env.example b/apps/axiomid/.env.example index fe4db38..62e1f1c 100644 --- a/apps/axiomid/.env.example +++ b/apps/axiomid/.env.example @@ -51,6 +51,10 @@ PI_WALLET_PRIVATE_SEED= # Set to `true` to point the Pi SDK at the sandbox environment. NEXT_PUBLIC_PI_SANDBOX=false +# 5. Development URL for Pi Sandbox communication +# Set to your sandbox subdomain from Pi Developer Portal (e.g., axiomid8992.pinet.com) +NEXT_PUBLIC_DEV_URL= + # ----------------------------------------------------------------------------- # Web3Auth (client) # ----------------------------------------------------------------------------- diff --git a/apps/axiomid/package.json b/apps/axiomid/package.json index cf8aa74..271daaa 100644 --- a/apps/axiomid/package.json +++ b/apps/axiomid/package.json @@ -47,6 +47,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@pinetwork/pi-sdk-js": "^2.0.0", "@prisma/client": "^6.0.0", "framer-motion": "^12.38.0", "next": "16.2.6", diff --git a/apps/axiomid/src/app/api/__tests__/auth-connect.test.ts b/apps/axiomid/src/app/api/__tests__/auth-connect.test.ts index 0d5d511..704fbab 100644 --- a/apps/axiomid/src/app/api/__tests__/auth-connect.test.ts +++ b/apps/axiomid/src/app/api/__tests__/auth-connect.test.ts @@ -20,10 +20,6 @@ jest.mock('@/lib/prisma', () => ({ }, })); -jest.mock('@/lib/tiers', () => ({ - calculateTier: jest.fn(), -})); - import { POST } from '../auth/connect/route'; describe('POST /api/auth/connect', () => { @@ -66,33 +62,6 @@ describe('POST /api/auth/connect', () => { expect(prisma.user.create).toHaveBeenCalled(); }); - it('should return existing user and update tier if necessary', async () => { - const existingUser = { - walletAddress: mockWalletAddress, - xp: 150, - tier: 'Ghost', - actions: [], - }; - - (prisma.user.findUnique as any).mockResolvedValue(existingUser); - (calculateTier as any).mockReturnValue('Spark'); - (prisma.user.update as any).mockResolvedValue({ - ...existingUser, - tier: 'Spark', - }); - - const req = { - json: async () => ({ walletAddress: mockWalletAddress }), - } as Request; - - const res = await POST(req); - const data = await res.json(); - - expect(res.status).toBe(200); - expect(data.user.tier).toBe('Spark'); - expect(prisma.user.update).toHaveBeenCalled(); - }); - it('should return 500 on internal error', async () => { (prisma.user.findUnique as any).mockRejectedValue(new Error('DB Error')); diff --git a/apps/axiomid/src/app/api/agent/activate/route.ts b/apps/axiomid/src/app/api/agent/activate/route.ts index 3aa907f..91bc019 100644 --- a/apps/axiomid/src/app/api/agent/activate/route.ts +++ b/apps/axiomid/src/app/api/agent/activate/route.ts @@ -47,7 +47,7 @@ export async function POST(request: Request) { const agent = await prisma.userAgent.update({ where: { userId: user.id }, data: { - status: 'active', + status: 'ACTIVE', lastActive: new Date(), }, select: { diff --git a/apps/axiomid/src/app/api/agent/main/route.ts b/apps/axiomid/src/app/api/agent/main/route.ts index 5894b90..b079d8d 100644 --- a/apps/axiomid/src/app/api/agent/main/route.ts +++ b/apps/axiomid/src/app/api/agent/main/route.ts @@ -50,7 +50,7 @@ export async function POST(request: Request) { if (existing) { const updated = await prisma.userAgent.update({ where: { userId: user.id }, - data: { name: name || existing.name, status: 'active', lastActive: new Date() }, + data: { name: name || existing.name, status: 'ACTIVE', lastActive: new Date() }, select: agentSelect, }); @@ -78,7 +78,7 @@ export async function POST(request: Request) { data: { userId: user.id, name: name || 'My Agent', - status: 'active', + status: 'ACTIVE', apiKeyHash, permissions: JSON.stringify(['claim', 'verify']), lastActive: new Date(), diff --git a/apps/axiomid/src/app/api/agent/route.ts b/apps/axiomid/src/app/api/agent/route.ts index e46e5bb..f1f70fd 100644 --- a/apps/axiomid/src/app/api/agent/route.ts +++ b/apps/axiomid/src/app/api/agent/route.ts @@ -62,7 +62,7 @@ export async function POST(request: Request) { data: { userId: user.id, name: name || 'My Agent', - status: 'inactive', + status: 'INACTIVE', apiKeyHash, permissions: JSON.stringify(['claim', 'verify']), }, diff --git a/apps/axiomid/src/app/api/pi/payment/cancel/route.ts b/apps/axiomid/src/app/api/pi/payment/cancel/route.ts new file mode 100644 index 0000000..adfe532 --- /dev/null +++ b/apps/axiomid/src/app/api/pi/payment/cancel/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; + +export async function POST(request: Request) { + try { + const { paymentId } = await request.json(); + if (!paymentId) { + return NextResponse.json({ error: "paymentId is required" }, { status: 400 }); + } + + return NextResponse.json({ success: true, paymentId }); + } catch (error) { + console.error("Error cancelling payment:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/apps/axiomid/src/app/api/pi/payment/error/route.ts b/apps/axiomid/src/app/api/pi/payment/error/route.ts new file mode 100644 index 0000000..26520a5 --- /dev/null +++ b/apps/axiomid/src/app/api/pi/payment/error/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; + +export async function POST(request: Request) { + try { + const { paymentId, error: errorMsg } = await request.json(); + if (!paymentId || !errorMsg) { + return NextResponse.json({ error: "paymentId and error are required" }, { status: 400 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error logging payment error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/apps/axiomid/src/app/api/pi/payment/incomplete/route.ts b/apps/axiomid/src/app/api/pi/payment/incomplete/route.ts new file mode 100644 index 0000000..0073afa --- /dev/null +++ b/apps/axiomid/src/app/api/pi/payment/incomplete/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from "next/server"; +import { getPiEnv } from "@/lib/pi/env"; + +export async function POST(request: Request) { + try { + const { paymentId, transactionId } = await request.json(); + if (!paymentId) { + return NextResponse.json({ error: "paymentId is required" }, { status: 400 }); + } + + const { apiKey } = getPiEnv(); + + const response = await fetch( + `https://api.minepi.com/v2/payments/${paymentId}`, + { + headers: { + Authorization: `Key ${apiKey}`, + }, + } + ); + + if (!response.ok) { + return NextResponse.json( + { error: "Failed to fetch incomplete payment" }, + { status: response.status } + ); + } + + const paymentData = await response.json(); + + return NextResponse.json({ success: true, payment: paymentData }); + } catch (error) { + console.error("Error handling incomplete payment:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/apps/axiomid/src/app/context/sandbox-provider.tsx b/apps/axiomid/src/app/context/sandbox-provider.tsx new file mode 100644 index 0000000..3f74098 --- /dev/null +++ b/apps/axiomid/src/app/context/sandbox-provider.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useEffect } from "react"; +import { initSandboxCompatibility } from "@/lib/pi-sandbox"; + +export function SandboxProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + initSandboxCompatibility(); + }, []); + + return <>{children}; +} diff --git a/apps/axiomid/src/app/context/wallet-context.tsx b/apps/axiomid/src/app/context/wallet-context.tsx index 28a5d76..04c140d 100644 --- a/apps/axiomid/src/app/context/wallet-context.tsx +++ b/apps/axiomid/src/app/context/wallet-context.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from "react"; import { Tier, getLevelProgress, getNextLevelXP } from "@/lib/tiers"; -import { ensurePiSdk } from "@/lib/pi-sdk"; +import { connectPi, runWalletTest, isPiSdkLoaded } from "@/lib/pi-sdk"; export interface User { id: string; @@ -32,34 +32,27 @@ interface WalletContextType { activateAgent: () => Promise; levelProgress: number; nextXP: number | null; + walletLogs: string[]; + runWalletTest: () => Promise; + clearWalletLogs: () => void; } const WalletContext = createContext(null); -function getSandboxFlag(): boolean { +function checkPiBrowser(): boolean { + if (typeof navigator === "undefined") return false; + if (isPiSdkLoaded()) return true; if (process.env.NEXT_PUBLIC_PI_SANDBOX === "true") return true; - if (typeof window !== "undefined") { - const params = new URLSearchParams(window.location.search); - if (params.get("sandbox") === "true") return true; + + try { + if (window.self !== window.top) return true; + } catch (e) { + // Cross-origin iframe check might throw, meaning we are in an iframe + return true; } - return false; -} - -const SANDBOX = getSandboxFlag(); -const AUTH_TIMEOUT_MS = 15000; - -function detectPiBrowser(): boolean { - if (typeof navigator === "undefined") return false; - return /Pi Browser|minepi/i.test(navigator.userAgent); -} - -function withTimeout(promise: Promise, ms: number): Promise { - return Promise.race([ - promise, - new Promise((_, reject) => - setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms) - ), - ]); + + const ua = navigator.userAgent; + return /Pi Browser|minepi/i.test(ua); } export function WalletProvider({ children }: { children: ReactNode }) { @@ -71,143 +64,106 @@ export function WalletProvider({ children }: { children: ReactNode }) { const levelProgress = user ? getLevelProgress(user.xp, user.tier) : 0; const nextXP = user ? getNextLevelXP(user.tier) : null; - const authAttempted = useRef(false); + const [walletLogs, setWalletLogs] = useState([]); - useEffect(() => { - setIsPiBrowser(detectPiBrowser()); - console.log("[AUTH DEBUG] SANDBOX flag:", SANDBOX, "| env:", process.env.NEXT_PUBLIC_PI_SANDBOX, "| url:", window.location.search.includes("sandbox=true")); + const pushLog = useCallback((msg: string) => { + setWalletLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]); }, []); - useEffect(() => { - setIsLoading(false); - }, []); + const clearWalletLogs = useCallback(() => setWalletLogs([]), []); const connectWallet = useCallback(async () => { setIsConnecting(true); setError(null); - - const debug = (label: string, data?: unknown) => { - console.log(`[AUTH DEBUG] ${label}`, data ?? ""); - }; + pushLog("بدء الاتصال بالمحفظة..."); try { - let walletAddress = ""; - let piUid = ""; - let piUsername = ""; - let accessToken = ""; - - const inPiBrowser = detectPiBrowser(); - const isSandbox = getSandboxFlag(); - const inIframe = typeof window !== "undefined" && window.self !== window.parent; - - debug("inPiBrowser", inPiBrowser); - debug("isSandbox", isSandbox); - debug("inIframe", inIframe); - debug("hasPi", typeof window !== "undefined" && !!window.Pi); - - const usePi = inPiBrowser || (isSandbox && inIframe); - debug("usePi", usePi); - - if (usePi) { - debug("loading Pi SDK..."); - await withTimeout(ensurePiSdk(isSandbox), AUTH_TIMEOUT_MS); - const pi = typeof window !== "undefined" ? (window as any).Pi : undefined; - debug("Pi SDK loaded", !!pi?.authenticate); - if (!pi?.authenticate) throw new Error("Pi SDK not available"); - - debug("calling Pi.authenticate..."); - const scopes = isSandbox - ? ["username", "payments"] - : ["username", "payments", "wallet_address"]; - debug("scopes requested:", scopes); - const auth = (await withTimeout( - pi.authenticate({ - scope: scopes, - onIncompletePaymentFound: (payment: any) => { - console.warn("Incomplete payment:", payment?.identifier); - } - }), - AUTH_TIMEOUT_MS - )) as any; - debug("Pi.authenticate done", { uid: auth?.user?.uid, username: auth?.user?.username }); - - piUid = auth.user.uid; - piUsername = auth.user.username; - accessToken = auth.accessToken; - walletAddress = `pi:${piUid}`; - } else { - debug("using demo wallet"); - // Reuse stored wallet if available, otherwise generate new demo wallet + const inPiBrowser = checkPiBrowser(); + pushLog(`حالة Pi Browser: ${inPiBrowser ? "نعم ✅" : "لا"}`); + + if (!inPiBrowser) { + pushLog("وضع التجربة (non-Pi browser)..."); const storedWallet = localStorage.getItem("axiomid_wallet"); - if (storedWallet && storedWallet.startsWith("demo:")) { - walletAddress = storedWallet; - } else { - walletAddress = `demo:${crypto.randomUUID().slice(0, 8)}`; - } + const walletAddress = storedWallet && storedWallet.startsWith("demo:") + ? storedWallet + : `demo:${crypto.randomUUID().slice(0, 8)}`; + localStorage.setItem("axiomid_wallet", walletAddress); + pushLog(`محفظة مؤقتة: ${walletAddress}`); + + const res = await fetch("/api/auth/connect", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ walletAddress }), + }); + + if (!res.ok) throw new Error("Demo auth failed"); + const data = await res.json(); + pushLog(`تم تسجيل الدخول بنجاح ✅`); + setUser(data.user); + return; } - debug("saving wallet to localStorage", walletAddress); - localStorage.setItem("axiomid_wallet", walletAddress); - if (accessToken) localStorage.setItem("axiomid_token", accessToken); - - const endpoint = usePi ? "/api/auth/pi" : "/api/auth/connect"; - const body = usePi - ? JSON.stringify({ accessToken, uid: piUid, walletAddress, username: piUsername }) - : JSON.stringify({ walletAddress, piUid, piUsername, accessToken }); - debug(`POST ${endpoint}`, { walletAddress, hasAccessToken: !!accessToken }); - const res = await fetch(endpoint, { + pushLog("جاري التوثيق عبر Pi SDK..."); + const { accessToken, user: piUser } = await connectPi(pushLog); + const walletAddress = `pi:${piUser.uid}`; + pushLog(`عنوان المحفظة: ${walletAddress}`); + + pushLog("جاري التحقق من صحة التوثيق مع السيرفر..."); + const res = await fetch("/api/auth/pi", { method: "POST", headers: { "Content-Type": "application/json" }, - body, + body: JSON.stringify({ + accessToken, + uid: piUser.uid, + username: piUser.username, + walletAddress, + }), }); if (!res.ok) { const err = await res.json(); - debug(`${endpoint} failed`, err); throw new Error(err.error || "Authentication failed"); } const data = await res.json(); - debug(`${endpoint} success`, data.user); + localStorage.setItem("axiomid_wallet", walletAddress); setUser(data.user); + pushLog(`✅ تم توثيق المحفظة بنجاح!`); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Connection failed"; - debug("connectWallet error", message); - console.error(err); + console.error("Auth error:", message); + pushLog(`❌ خطأ: ${message}`); setError(message); } finally { setIsConnecting(false); } + }, [pushLog]); + + const runTest = useCallback(async () => { + clearWalletLogs(); + pushLog("🚀 بدء اختبار المحفظة الشامل..."); + try { + await runWalletTest(pushLog); + } catch (err: any) { + pushLog(`❌ خطأ غير متوقع: ${err?.message || err}`); + } + }, [pushLog, clearWalletLogs]); + + useEffect(() => { + const inPiBrowser = checkPiBrowser(); + setIsPiBrowser(inPiBrowser); + setIsLoading(false); }, []); useEffect(() => { if (authAttempted.current) return; authAttempted.current = true; - const inPiBrowser = detectPiBrowser(); - const isSandbox = getSandboxFlag(); - const inIframe = typeof window !== "undefined" && window.self !== window.parent; - const isPiEnv = inPiBrowser || (isSandbox && inIframe); - console.log("[AUTH DEBUG] auto-auth: SANDBOX=", isSandbox, "PiBrowser=", inPiBrowser, "inIframe=", inIframe, "isPiEnv=", isPiEnv); + const inPiBrowser = checkPiBrowser(); - if (isPiEnv) { + if (inPiBrowser) { connectWallet(); - } else { - const storedWallet = localStorage.getItem("axiomid_wallet"); - const storedToken = localStorage.getItem("axiomid_token"); - if (storedWallet) { - fetch("/api/auth/connect", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ walletAddress: storedWallet, accessToken: storedToken || undefined }), - }) - .then((res) => res.json()) - .then((data) => { - if (data.user) setUser(data.user); - }) - .catch(() => {}); - } } }, [connectWallet]); @@ -250,12 +206,11 @@ export function WalletProvider({ children }: { children: ReactNode }) { const createAgent = useCallback(async (name?: string) => { if (!user) return false; - const token = localStorage.getItem("axiomid_token"); try { const res = await fetch("/api/agent", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ walletAddress: user.walletAddress, name, accessToken: token || undefined }), + body: JSON.stringify({ walletAddress: user.walletAddress, name }), }); if (!res.ok) return false; await refreshUser(); @@ -267,12 +222,11 @@ export function WalletProvider({ children }: { children: ReactNode }) { const activateAgent = useCallback(async () => { if (!user) return false; - const token = localStorage.getItem("axiomid_token"); try { const res = await fetch("/api/agent/activate", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ walletAddress: user.walletAddress, accessToken: token || undefined }), + body: JSON.stringify({ walletAddress: user.walletAddress }), }); if (!res.ok) return false; await refreshUser(); @@ -283,27 +237,35 @@ export function WalletProvider({ children }: { children: ReactNode }) { }, [user, refreshUser]); return ( - + {children} ); } +export function useWalletLogs(): string[] { + const ctx = useContext(WalletContext); + return ctx?.walletLogs ?? []; +} + export function useWallet() { const ctx = useContext(WalletContext); if (!ctx) { diff --git a/apps/axiomid/src/app/dashboard/page.tsx b/apps/axiomid/src/app/dashboard/page.tsx index 6bded0d..69960da 100644 --- a/apps/axiomid/src/app/dashboard/page.tsx +++ b/apps/axiomid/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useWallet } from "../context/wallet-context"; -import { createPiPayment } from "@/lib/pi-wallet"; +import { createPiPayment } from "@/lib/pi-sdk"; import Link from "next/link"; import skillsData from "@/data/skills.json"; import personasData from "@/data/personas.json"; @@ -39,7 +39,10 @@ export default function Dashboard() { isPiBrowser, createAgent, activateAgent, - refreshUser + refreshUser, + runWalletTest, + walletLogs, + clearWalletLogs } = useWallet(); const [logs, setLogs] = useState(INITIAL_LOGS); @@ -71,6 +74,16 @@ export default function Dashboard() { return () => clearInterval(interval); }, []); + // Pipe wallet logs to terminal as they come in + const prevLogsLength = useRef(0); + useEffect(() => { + if (walletLogs.length > prevLogsLength.current) { + const newLogs = walletLogs.slice(prevLogsLength.current); + setLogs((prev) => [...prev, ...newLogs]); + prevLogsLength.current = walletLogs.length; + } + }, [walletLogs]); + // Scroll terminal to bottom useEffect(() => { terminalEndRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -94,7 +107,7 @@ export default function Dashboard() { let response = ""; if (cmd === "help") { - response = "Available commands: status, did, balance, skills, personas, memory, conscience, mcp, clear, ping"; + response = "Available commands: status, did, balance, skills, personas, memory, conscience, mcp, wallet-test, wallet-logs, clear, ping"; setLogs((prev) => [...prev, `[SYSTEM] ${response}`]); } else if (cmd === "status") { response = `Agentic OS v1.0.0. User: ${user?.piUsername || "anonymous"}. Tier: ${user?.tier || "None"}. Agent: ${user?.agent ? user.agent.name : "None"}. Status: active.`; @@ -108,6 +121,21 @@ export default function Dashboard() { } else if (cmd === "ping") { response = "pong. latency: 12ms. resonance: 369hz."; setLogs((prev) => [...prev, `[SYSTEM] ${response}`]); + } else if (cmd === "wallet-test") { + prevLogsLength.current = 0; + clearWalletLogs(); + setLogs((prev) => [...prev, "[SYSTEM] 🚀 تشغيل اختبار المحفظة الشامل..."]); + runWalletTest(); + } else if (cmd === "wallet-logs") { + if (walletLogs.length === 0) { + setLogs((prev) => [...prev, "[SYSTEM] لا توجد سجلات. استخدم 'wallet-test' أولاً."]); + } else { + setLogs((prev) => [...prev, `[SYSTEM] === سجلات اختبار المحفظة (${walletLogs.length}) ===`]); + for (const log of walletLogs) { + setLogs((prev) => [...prev, log]); + } + setLogs((prev) => [...prev, "[SYSTEM] === انتهت السجلات ==="]); + } } else if (cmd === "mcp") { setLogs((prev) => [ ...prev, @@ -265,19 +293,19 @@ export default function Dashboard() { }; return ( -
+
{/* 1. SIDEBAR NAVIGATION */} -