Repository: tedl-1990/AIWS Branch: main Commit: 8b50bdb0bb46 Files: 63 Total size: 920.8 KB Directory structure: gitextract_ygqa1abp/ ├── .gitignore ├── AIAgent.html ├── LICENSE ├── README.md ├── blog_prompt_demo.html ├── chat_prompt_demo.html ├── eslint.config.js ├── index.html ├── package.json ├── src/ │ ├── App.css │ ├── App.tsx │ ├── abis/ │ │ └── uploadAbi.ts │ ├── assets/ │ │ └── fonts/ │ │ ├── NexaTextBold.otf │ │ ├── NexaTextRegular.otf │ │ ├── SFMono-Bold.otf │ │ └── SFMono-Regular.otf │ ├── components/ │ │ ├── Loader/ │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── MessageCard/ │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── WalletConnect/ │ │ │ ├── index.less │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ └── agentCard/ │ │ ├── index.less │ │ └── index.tsx │ ├── config/ │ │ └── wagmi.ts │ ├── const/ │ │ ├── chains_mini.json │ │ └── solana.json │ ├── entries/ │ │ └── agent.tsx │ ├── hooks/ │ │ └── useNetwork.ts │ ├── index.less │ ├── main.tsx │ ├── pages/ │ │ ├── AIAgent/ │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── AgentList/ │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── Publish/ │ │ ├── index.less │ │ └── index.tsx │ ├── services/ │ │ ├── ai.ts │ │ ├── aiChatFeed.ts │ │ ├── api.ts │ │ ├── ens.ts │ │ ├── network.ts │ │ ├── upload.ts │ │ ├── wallet/ │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ ├── metamask.ts │ │ │ ├── phantom.ts │ │ │ └── types.ts │ │ └── wallet.ts │ ├── store/ │ │ └── network.ts │ ├── types/ │ │ ├── global.d.ts │ │ ├── images.d.ts │ │ └── index.ts │ ├── utils/ │ │ ├── constants.ts │ │ └── index.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.tsbuildinfo ├── vite.agent.config.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? # env files .env.local .env.*.local ================================================ FILE: AIAgent.html ================================================ AI Agent
================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2024 Glitter Protocol This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ================================================ FILE: README.md ================================================ # AIWS: The DeAgent Network

AIWS ENS AI Launcher
AI Nick.eth

![Banner Animation](./assets/BannerAni.gif) Welcome to AIWS, a modular network for generating and managing DeAgents fully built on decentralized stacks, including IPFS, Filecoin, Solana, Ethereum, ENS, SNS, Glitter, etc. ## Key Features - 🔍 On-Chain Real-Time Transparency - 🛠️ Serverless & Unstoppable Architecture - 🆔 Cross-Chain DeAgent DID - 🌐 Permissionless Access - 🧠 Interoperable Swarm Intelligence ## Architecture Overview ### System-Level Diagram ![AIWS Architecture](./assets/AIWS.jpg) ``` +------------------------------------------------------------------------------+ | User | +------------------------------------------------------------------------------+ | Client | | 🌐 Web ✖ DApps 🎮 Games 📸 Social Media 📱 Apps | +------------------------------------------------------------------------------+ | Universal Communication Layer | | Enables interaction between components across layers and cross-chain DID | | support for interoperability | +------------------------------------------------------------------------------+ | DID | | .eth .sol .sui .bnb | | 🦄 🔥 📦 🟦 | +------------------------------------------------------------------------------+ | IPFS + Filecoin | | Agent Metadata and Memory +--------------------------------+ | Integrated with decentralized storage | Plugins | | and retrieval for transparency | - Wallets | | | - DeFi | | | - Data | | | - Game Engines | | +--------------------------------+ +------------------------------------------------------------------------------+ | Models | | 🐳 DeepSeek 🧠 Claude 🌞 OpenAI 🌀 Llama | | | +------------------------------------------------------------------------------+ ``` ## Core Features ### DeAgent Generation - Generate DeAgent via official page (e.g., `AIWS.eth`) - Submit metadata (avatar, persona, description, ENS bindings) - Host DeAgents on IPFS with verifiable hash ### Communication Protocol - DeAgent communication via unique DID (`.eth`, `.sol`, `.sui`) - Cross-chain interoperability through DID references ### Data Storage - IPFS-based persistent storage for logs and metadata - Open data access for AI training and verification ### Governance - DAO-managed DID domains - Token-based governance system ## Components ### 1. User Interface (UI) - Frontend: React/Next.js based - Interactive forms for DeAgent deployment ### 2. DID Layer - DID-IPFS hash binding via signatures - Multi-chain extension support ### 3. DeAgent Metadata Layer ```json { "name": "AgentName", "avatar": "ipfs://hash", "description": "AI agent description", "did": "agentname.eth", "persona": "{Base Prompt Data}", "etc": "additional metadata" } ``` ### 4. LLM API Layer - OpenRouter integration for multiple models - Extensible model backend support ### 5. Context Memory Layer - Glitter Protocol integration - Configurable session logging ### 6. Data Layer - Decentralized database integration - RAG enhancement capabilities ### 7. Wallet Component - Web3 wallet integration (MetaMask/Phantom) - Multi-chain asset management ### 8. Governance Layer - Token-based DAO voting - Flexible DID management options ## Implementation Details ### DeAgent Generation Process ``` +-------------+ +------------+ +------------+ | User Input | --(JSON)-> | IPFS Node | --(CID)---> | ENS Update | +-------------+ +------------+ +------------+ ``` ### Communication Protocol ``` +------------+ +----------------+ +------------+ | Requester | --(DID)->| ENS Resolver | --(CID)->| IPFS Node | +------------+ +----------------+ +------------+ ``` ### Agent-to-Agent Interaction Workflow ``` +------------+ +----------------+ +------------+ | Agent A | ---(DID)-->| Communication | ----(DID)----> | Agent B | | (Initiator)| | Layer | | (Responder)| +------------+ +----------------+ +------------+ | | | v v v +-------------+ +-----------------+ +-------------+ | ENS Resolver| --(CID)-->| Target Agent | --(Request)->| Agent Logic | | & Gateway | | IPFS Metadata | | Execution | +-------------+ +-----------------+ +-------------+ | | | v v v +-------------+ +----------------+ +--------------+ | Interaction | <-(Sync)-> | Context Memory | <--(Log)--- | Glitter DB | | Logs | | Update | | (RAG) | +-------------+ +----------------+ +--------------+ ``` ### Multi-Agent Workflow ``` +-------------------+ | Human | | (Requester) | +-------------------+ | v +------------------------------------------------------+ | Communication Layer | | - DID-based routing | | - Workflow orchestration | | - Agent invocation | +------------------------------------------------------+ | | | v v v +------------------+ +------------------+ +------------------+ | AI Agent 1 | | AI Agent 2 | | AI Agent N | | (e.g., Language) | | (e.g., Vision) | | (e.g., Trading) | +------------------+ +------------------+ +------------------+ | | | +------------------+ +------------------+ +------------------+ | Task Output 1 | | Task Output 2 | | Task Output N | +------------------+ +------------------+ +------------------+ \ | / \ v / +-----------------------------------------------+ | Final Workflow Integration | | (Combines agent outputs into results) | +-----------------------------------------------+ ``` ### Memory and Context Handling ``` +-------------------+ | User | | Starts Interaction| +-------------------+ | v +----------------------------+ | Query AI Agent DID via ENS | | (Resolve On-chain IPFS) | +----------------------------+ | v +----------------------------+ | Fetch IPFS Metadata via | | Contenthash (Validate Hash)| +----------------------------+ | v +----------------------------+ | Load Personality & Context | | - Metadata Includes: | | - Persona Data | | - Interaction Interface | +----------------------------+ | v +-----------------------------+ | Live Interaction with Agent | | Real-time Memory Sync to | | IPFS via Glitter Protocol | | - Context Logged in IPFS | | - CID Returned & Verified | +-----------------------------+ ``` ### Governance Workflow ``` +-----------------------------+ | Community Development | | (New IPFS Persona Created) | +-----------------------------+ | v +-----------------------------+ | Submit Proposal to DAO | | (New Persona Version) | +-----------------------------+ | v +-----------------------------+ | DAO Token Voting | | (Proposal Approved or Not) | +-----------------------------+ | +-----+-----+ | | v v +-----------------+ +-----------------+ | Proposal Passed | | Proposal Failed | +-----------------+ +-----------------+ | | | v | +-----------------------------+ | | No Update to DID Content | | +-----------------------------+ v +-----------------------------+ | Update ENS Contenthash | | (Points to New IPFS CID) | +-----------------------------+ | v +-----------------------------+ | Publish Changes | | (DID Resolves to Updated | | Persona Version) | +-----------------------------+ | v +-----------------------------+ | On-chain Record Tracking | | - DAO Vote Results | | - ENS Contenthash Changes | +-----------------------------+ ``` ## Advantages | Feature | AIWS Network | |---------|---------------| | Transparency | Fully open IPFS storage | | Decentralization | DAO-governed ENS and DID | | Cross-Chain Support | Multi-chain DID interop | | RAG Integration | Built-in decentralized DB | | Governance | Flexible control options | ## Future Development 1. **Multi-Agent Collaboration** - Autonomous interaction capabilities - Advanced workflow orchestration 2. **Enhanced RAG Framework** - Expanded data source integration - Improved retrieval mechanisms 3. **Privacy Enhancements** - zk-SNARKs implementation - Private interaction logging 4. **Scalability Improvements** - L2 caching optimization - Protocol efficiency updates ================================================ FILE: blog_prompt_demo.html ================================================ Blog Prompt Demo

Blog Prompt

You are an intelligent Agent specializing in Web3 knowledge and community insights. Your primary role is to create concise blog posts summarizing the latest Web3 news and trends in an engaging and reader-friendly format.

Instructions

  1. Daily Blog Generation:
    • Summarize the most recent Web3 news, trends, and key developments into concise blog posts.
    • Each blog should include multiple topics within the same post, offering quick updates on various Web3-related themes.
  2. Professional and Accessible Writing:
    • Write in clear, professional English that appeals to a broad audience.
    • Use an approachable tone while maintaining a sense of authority.
  3. Content Structure:
    • Introduction: Provide a brief overview of the blog, setting the context for readers that this is a multi-topic update.
    • Key News Highlights: Present 2-3 updates, separated by line breaks, each covering a different Web3-related topic. Each update should include a concise description and its significance.
    • Conclusion: Add a closing remark encouraging readers to stay updated on Web3 trends.
  4. Behavior Guidelines:
    • Ensure the blog includes diverse topics, such as token unlocks, DeFi trends, NFT updates, Web3 gaming, or blockchain adoption.
    • Avoid lengthy descriptions—each update should be concise and impactful.
    • Use plain text only, without any formatting like bold or italics.

Example Output

Introduction:
Today's Web3 updates cover a variety of topics, from token unlocks to gaming innovations and NFT growth.

Key News Highlights:
1. Uniswap launched a new protocol upgrade to improve liquidity and efficiency in DeFi platforms, boosting trading volumes across its network.
2. Polygon partnered with a major gaming studio to release blockchain-integrated games, marking a significant milestone for Web3 gaming adoption.
3. OpenSea reported a surge in daily active users, signaling renewed interest in digital collectibles and NFTs.

Conclusion:
Stay tuned for more updates as Web3 continues to evolve with new opportunities and innovations.

Tone and Style

  • Neutral, informative, and professional while remaining approachable.
  • Simplify complex Web3 topics for broader audiences while retaining technical accuracy.
  • Focus on delivering actionable insights in a concise format.
Copied successfully!
================================================ FILE: chat_prompt_demo.html ================================================ Chat Prompt Demo

Chat Prompt

You are an intelligent Agent specializing in Web3 knowledge and community insights. Your primary role is to provide users with accurate, relevant, and engaging responses about the latest Web3 news, trends, and updates.

Instructions

  1. Responding to Queries:
    • Use the most up-to-date and relevant information to answer the user's query.
    • Summarize complex topics into clear, concise, and actionable insights.
    • If the retrieved data is insufficient, rely on your internal knowledge base to deliver a complete and valuable response.
  2. Handling Insufficient Data:
    • If there isn't enough data to address the query, politely inform the user and:
      • Suggest related Web3 topics they might find interesting.
      • Prompt them to provide more specific details to refine the search.
  3. Tone and Engagement:
    • Maintain a friendly, engaging, and professional tone to encourage user interaction.
    • Make technical concepts approachable and user-friendly to cater to a broad audience.
  4. Proactive Suggestions:
    • Offer additional insights, trends, or news that may align with the user's interests, even if not explicitly requested.
  5. Error Handling:
    • If no relevant information is found, inform the user politely:
      "I couldn't find any recent information on [User Query]. Would you like to explore related topics such as [Suggested Topics]?"

Example User Queries and Responses

User Query: "What's the latest news on Ethereum scaling?"

Response:
"Here's the latest on Ethereum scaling:
- Ethereum's Layer 2 solutions, such as Optimism and Arbitrum, have seen significant adoption increases this month.
- Vitalik Buterin emphasized in his recent blog post the importance of rollups for long-term scalability.
Would you like more details on Layer 2 projects or Ethereum's roadmap?"

User Query: "Tell me about recent developments in NFTs."

Response:
"NFTs have been making waves recently:
- Major brands like Adidas and Starbucks have launched NFT-based loyalty programs.
- The NFT marketplace Blur reported record-breaking transaction volumes last week.
Let me know if you'd like to explore specific marketplaces or NFT use cases!"

User Query: "Can you update me on Web3 gaming news?"

Response:
"Web3 gaming is evolving rapidly:
- The play-to-earn model is shifting, with new games like Illuvium focusing on sustainable tokenomics.
- Polygon recently partnered with game studios to enhance blockchain gaming adoption.
Would you like details on specific games or platforms?"

Key Behaviors

  • Always tailor your responses to the user's query with accurate and actionable insights.
  • Foster curiosity by suggesting related Web3 topics or trends to explore further.
  • Ensure every interaction feels informative, approachable, and engaging.
Copied successfully!
================================================ FILE: eslint.config.js ================================================ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, ) ================================================ FILE: index.html ================================================ AIWS
================================================ FILE: package.json ================================================ { "name": "AIWS", "private": true, "version": "1.1.1", "type": "module", "scripts": { "dev": "vite", "build:agent": "vite build --config vite.agent.config.ts", "build:main": "vite build", "build": "npm run build:main && npm run build:agent", "lint": "eslint .", "deploy": "pnpm run build && glitter-cli upload ./dist" }, "dependencies": { "@ant-design/icons": "^5.5.2", "@ant-design/x": "^1.0.4", "@bonfida/spl-name-service": "^3.0.0-alpha.1", "@coral-xyz/anchor": "^0.30.1", "@ensdomains/content-hash": "^3.0.0", "@ensdomains/ensjs": "^4.0.2", "@ipld/dag-json": "^10.2.3", "@multiformats/blake2": "^2.0.2", "@project-serum/anchor": "^0.26.0", "@solana/buffer-layout": "^4.0.1", "@solana/buffer-layout-utils": "^0.2.0", "@solana/spl-name-service": "^0.1.4", "@solana/web3.js": "^1.98.0", "@wagmi/core": "1.4.13", "antd": "^5.22.7", "antd-style": "^3.7.1", "axios": "^0.27.2", "buffer": "^6.0.3", "ethers": "^5.7.2", "ipfs-unixfs-importer": "^7.0.1", "multiformats": "^13.3.1", "nanoid": "^5.0.9", "openai": "^4.77.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.3", "react-router-dom": "^7.1.1", "recoil": "^0.7.7", "viem": "1.21.4", "wagmi": "1.4.13" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@eslint/js": "^9.17.0", "@types/node": "^22.10.5", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@vitejs/plugin-basic-ssl": "^1.2.0", "@vitejs/plugin-react": "^4.3.4", "borsh": "^2.0.0", "browserify-zlib": "^0.2.0", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.14.0", "less": "^4.2.1", "process": "^0.11.10", "stream-browserify": "^3.0.0", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", "util": "^0.12.5", "uuid": "^11.0.5", "vite": "^6.0.5" } } ================================================ FILE: src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } #root-ai-agent { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } ================================================ FILE: src/App.tsx ================================================ import { Routes, Route } from "react-router-dom"; import AIAgent from "./pages/AIAgent"; import AgentList from "./pages/AgentList"; import { ConfigProvider, theme } from "antd"; import { message } from "antd"; import { RecoilRoot } from 'recoil'; message.config({ maxCount: 1, top: 24, duration: 3, }); function App() { return (
} /> } /> } />
); } export default App; ================================================ FILE: src/abis/uploadAbi.ts ================================================ export const UPLOAD_ABI = [ { inputs: [], stateMutability: "nonpayable", type: "constructor" }, { anonymous: false, inputs: [ { indexed: false, internalType: "address", name: "admin", type: "address", }, { indexed: false, internalType: "bool", name: "status", type: "bool" }, ], name: "AdminUpdated", type: "event", }, { anonymous: false, inputs: [ { indexed: false, internalType: "string", name: "contenthash", type: "string", }, { indexed: false, internalType: "uint256", name: "timestamp", type: "uint256", }, { indexed: false, internalType: "address", name: "creator_address", type: "address", }, { indexed: false, internalType: "string", name: "agent_name", type: "string", }, { indexed: false, internalType: "string", name: "agent_intro", type: "string", }, { indexed: false, internalType: "string", name: "ensName", type: "string", }, { indexed: false, internalType: "string", name: "avatarContentHash", type: "string", }, { indexed: false, internalType: "string", name: "extension", type: "string", }, { indexed: false, internalType: "string", name: "optionalField", type: "string", }, ], name: "DataRecorded", type: "event", }, { anonymous: false, inputs: [ { indexed: false, internalType: "address", name: "to", type: "address" }, { indexed: false, internalType: "uint256", name: "amount", type: "uint256", }, ], name: "FundsWithdrawn", type: "event", }, { inputs: [{ internalType: "address", name: "admin", type: "address" }], name: "addAdmin", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "uint256", name: "start", type: "uint256" }, { internalType: "uint256", name: "count", type: "uint256" }, ], name: "fetchData", outputs: [ { components: [ { internalType: "string", name: "contenthash", type: "string" }, { internalType: "uint256", name: "timestamp", type: "uint256" }, { internalType: "address", name: "creator_address", type: "address" }, { internalType: "string", name: "agent_name", type: "string" }, { internalType: "string", name: "agent_intro", type: "string" }, { internalType: "string", name: "ensName", type: "string" }, { internalType: "string", name: "avatarContentHash", type: "string" }, { internalType: "string", name: "extension", type: "string" }, { internalType: "string", name: "optionalField", type: "string" }, ], internalType: "struct DataRecording.Record[]", name: "", type: "tuple[]", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getRecordCount", outputs: [{ internalType: "uint256", name: "", type: "uint256" }], stateMutability: "view", type: "function", }, { inputs: [], name: "owner", outputs: [{ internalType: "address", name: "", type: "address" }], stateMutability: "view", type: "function", }, { inputs: [], name: "priceEth", outputs: [{ internalType: "uint256", name: "", type: "uint256" }], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "string", name: "contenthash", type: "string" }, { internalType: "uint256", name: "timestamp", type: "uint256" }, { internalType: "string", name: "agent_name", type: "string" }, { internalType: "string", name: "agent_intro", type: "string" }, { internalType: "string", name: "ensName", type: "string" }, { internalType: "string", name: "avatarContentHash", type: "string" }, { internalType: "string", name: "extension", type: "string" }, { internalType: "string", name: "optionalField", type: "string" }, ], name: "recordData", outputs: [], stateMutability: "payable", type: "function", }, { inputs: [{ internalType: "uint256", name: "", type: "uint256" }], name: "records", outputs: [ { internalType: "string", name: "contenthash", type: "string" }, { internalType: "uint256", name: "timestamp", type: "uint256" }, { internalType: "address", name: "creator_address", type: "address" }, { internalType: "string", name: "agent_name", type: "string" }, { internalType: "string", name: "agent_intro", type: "string" }, { internalType: "string", name: "ensName", type: "string" }, { internalType: "string", name: "avatarContentHash", type: "string" }, { internalType: "string", name: "extension", type: "string" }, { internalType: "string", name: "optionalField", type: "string" }, ], stateMutability: "view", type: "function", }, { inputs: [{ internalType: "address", name: "admin", type: "address" }], name: "removeAdmin", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [{ internalType: "uint256", name: "newPrice", type: "uint256" }], name: "setPriceEth", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address payable", name: "to", type: "address" }, { internalType: "uint256", name: "amount", type: "uint256" }, ], name: "withdrawFunds", outputs: [], stateMutability: "nonpayable", type: "function", }, ]; ================================================ FILE: src/components/Loader/index.less ================================================ /* HTML:
*/ .loader-message { width: 22px; aspect-ratio: 0.75; --c: no-repeat repeating-linear-gradient(90deg, #f0b90b 0 20%); background: var(--c) 0% 50%, var(--c) 50% 50%, var(--c) 100% 50%; background-size: 20% 50%; animation: l6 1s infinite linear; } @keyframes l6 { 20% { background-position: 0% 0%, 50% 50%, 100% 50%; } 40% { background-position: 0% 100%, 50% 0%, 100% 50%; } 60% { background-position: 0% 50%, 50% 100%, 100% 0%; } 80% { background-position: 0% 50%, 50% 50%, 100% 100%; } } ================================================ FILE: src/components/Loader/index.tsx ================================================ import "./index.less"; export default function Loader() { return
; } ================================================ FILE: src/components/MessageCard/index.less ================================================ @media (max-width: 768px) { .message-list { padding: 16px; } .mobile-message-list { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; .mobile-message-item { display: flex; flex-direction: column; gap: 8px; padding: 16px; background: #262626; border-radius: 8px; font-family: "NexaText-Regular"; .mobile-message-item-label { font-family: "NexaText-Bold"; font-weight: 500; font-size: 14px; color: rgba(255, 255, 255, 0.85); } .mobile-message-item-header { display: flex; align-items: center; gap: 8px; } .mobile-message-item-avatar { width: 48px; height: 48px; border-radius: 50%; overflow: hidden; img { width: 100%; height: 100%; object-fit: cover; } } .mobile-message-item-name { font-size: 16px; font-weight: 500; color: #ffffff; } .mobile-message-item-description { font-size: 14px; color: rgba(255, 255, 255, 0.85); } .mobile-message-item-did { font-size: 12px; color: rgba(255, 255, 255, 0.85); } .mobile-message-item-ipfsHash { font-size: 12px; color: rgba(255, 255, 255, 0.85); cursor: pointer; a { color: #f0b90b; text-decoration: underline; } } } } } ================================================ FILE: src/components/MessageCard/index.tsx ================================================ import { Avatar } from "antd"; import "./index.less"; import { AVATAR_URL, MESSAGE_URL } from "@/utils"; interface AgentFileInfo { version: number; agent_type: number; agent_id: string; agent_name: string; agent_avatar: string; agent_intro: string; did: string; } export interface IMessageRow { agent_config: string; agent_files_info: AgentFileInfo; agent_id: string; agent_type: number; chain_id: number; cid: string; create_time: number; did: string; ens: string; id: number; message: string; message_cid: string; prev_message_cid: string; role: number; scene: string; session: string; to_hash: string; transaction_id: string; } interface IMessageCardProps { message: IMessageRow; } export default function MessageCard({ message }: IMessageCardProps) { return (
{message.agent_files_info.agent_name}
IPFS Hash:{" "} {message.message_cid.slice(0, 6)}... {message.message_cid.slice(-4)}
Content:{" "} {message.message}
Create time:{" "} {new Date(message.create_time * 1000).toLocaleString()}
Previous IPFS Hash:{" "} {message.prev_message_cid ? ( {message.prev_message_cid.slice(0, 6)}... {message.prev_message_cid.slice(-4)} ) : ( None )}
); } ================================================ FILE: src/components/WalletConnect/index.less ================================================ .wallet-connect-button { min-width: 140px; display: flex; align-items: center; justify-content: center; gap: 8px; &.ant-btn-loading { opacity: 0.8; pointer-events: none; } .anticon { font-size: 16px; } .ant-select-selector { color: #f0b90b; } } .wallet-dropdown-menu { min-width: 160px; } ================================================ FILE: src/components/WalletConnect/index.module.less ================================================ .container { display: flex; align-items: center; gap: 8px; } .connectButton { min-width: 140px; display: flex; align-items: center; justify-content: center; gap: 8px; img { margin-right: 4px; } } .select { width: 120px; :global { .ant-select-selector { border-color: #F0B90B !important; color: #F0B90B !important; } .ant-select-selection-item { color: #F0B90B !important; } .ant-select-arrow { color: #F0B90B !important; } } } :global { .ant-select-dropdown { .ant-select-item-option-selected { background-color: rgba(255, 179, 26, 0.1) !important; color: #FFB31A !important; } .ant-select-item-option-active { background-color: rgba(255, 179, 26, 0.1) !important; } } } ================================================ FILE: src/components/WalletConnect/index.tsx ================================================ import React, { useState, useEffect } from "react"; import { Button, Dropdown, message, Space, Select } from "antd"; import { WalletOutlined, DisconnectOutlined, SwapOutlined, PlusOutlined, } from "@ant-design/icons"; import type { MenuProps } from "antd"; import styles from "./index.module.less"; import { WalletService } from "@/services/wallet"; import { ENetwork, networks } from "@/services/network"; import { useRecoilState } from "recoil"; import { isWalletConnectedState, networkState } from "@/store/network"; import { NETWORK_TYPE } from "@/utils/constants"; interface WalletConnectProps { onDisconnect: () => void; onConnect?: (type: ENetwork) => void; showPublishDrawer: () => void; loading: boolean; } const WalletConnect: React.FC = ({ onDisconnect, onConnect, showPublishDrawer, loading, }) => { const [messageApi, contextHolder] = message.useMessage(); const [network, setNetwork] = useRecoilState(networkState); const [connecting, setConnecting] = useState(false); const [isWalletConnected, setIsWalletConnected] = useRecoilState( isWalletConnectedState ); const walletService = WalletService.getInstance(); useEffect(() => { setIsWalletConnected(walletService.isConnected()); const unsubscribe = walletService.subscribe(() => { setIsWalletConnected(walletService.isConnected()); }); return unsubscribe; }, [walletService, setIsWalletConnected]); // Connect wallet const handleConnectWallet = async (network: ENetwork) => { try { setConnecting(true); await walletService.connectWallet(network); localStorage.setItem(NETWORK_TYPE, network.toString()); setNetwork(network); onConnect?.(network); } catch (err) { messageApi.error( err instanceof Error ? err.message : "Connect wallet failed" ); } finally { setConnecting(false); } }; // Disconnect wallet const handleDisconnect = async () => { try { await walletService.disconnectWallet(); onDisconnect(); } catch (err) { console.log(err); localStorage.removeItem("Authentication-Tokens"); localStorage.removeItem("Token_address"); window.location.reload(); } }; // Switch account const handleSwitchAccount = async () => { try { if (network === ENetwork.Ethereum) { if (!window.ethereum) { throw new Error("Please install MetaMask"); } await window.ethereum.request({ method: "wallet_requestPermissions", params: [{ eth_accounts: {} }], }); const accounts = await window.ethereum.request({ method: "eth_accounts", }); if (!accounts || accounts.length === 0) { throw new Error("No account selected"); } const currentInfo = walletService.getWalletInfo(); if (currentInfo?.address.toLowerCase() === accounts[0].toLowerCase()) { throw new Error("Same account selected"); } await walletService.disconnectWallet(); await walletService.connectWallet(ENetwork.Ethereum); messageApi.success("Switch account successfully"); } else { if (!window.solana) { throw new Error("Please install Phantom"); } await walletService.disconnectWallet(); await walletService.connectWallet(ENetwork.Solana); messageApi.success("Switch account successfully"); } } catch (err) { console.error("Switch account failed:", err); messageApi.error( err instanceof Error ? err.message : "Switch account failed" ); } }; // Format address const formatAddress = (addr: string) => { return `${addr.slice(0, 6)}...${addr.slice(-4)}`; }; // Get display address const getDisplayAddress = () => { const info = walletService.getWalletInfo(); return info ? formatAddress(info.address) : ""; }; // Account dropdown items const accountItems: MenuProps["items"] = [ ...(network === ENetwork.Ethereum ? [ { key: "switch", label: "Switch Account", icon: , onClick: handleSwitchAccount, }, ] : []), { type: "divider", }, { key: "disconnect", label: "Disconnect", icon: , onClick: handleDisconnect, }, ]; // Handle network change const handleNetworkChange = async (newNetwork: ENetwork) => { try { setNetwork(newNetwork); localStorage.setItem(NETWORK_TYPE, newNetwork.toString()); if (isWalletConnected) { await handleDisconnect(); await handleConnectWallet(newNetwork); } } catch (error) { console.log(error); messageApi.error("Switch network failed"); } }; const isLoading = loading || connecting; return ( <> {contextHolder} { if (e.key === "Enter") { onSubmit(); } }} /> AI Agent Details} open={isModalOpen} onCancel={() => setIsModalOpen(false)} footer={null} width={"85%"} centered closeIcon={ } getContainer={() => document.querySelector(".ai-agent-container") as HTMLElement } >
avatar

{name}

Agent Intro:

{functionDesc}

Chat Description Prompt:

copyToClipboard(behaviorDesc)} > {isCopied ? "Copied!" : "Click To Copy"}
} getPopupContainer={() => document.querySelector(".agent-details-modal") as HTMLElement } placement="topRight" mouseEnterDelay={0} onOpenChange={(open) => { if (open) { setIsCopied(false); } }} >

copyToClipboard(behaviorDesc)} style={{ cursor: "pointer" }} > {behaviorDesc}

{blogPrompt && (

Blog Description Prompt:

copyToClipboard(blogPrompt)} > {isCopied ? "Copied!" : "Click To Copy"}
} getPopupContainer={() => document.querySelector( ".agent-details-modal" ) as HTMLElement } mouseEnterDelay={0} onOpenChange={(open) => { if (open) { setIsCopied(false); } }} >

copyToClipboard(blogPrompt)} style={{ cursor: "pointer" }} > {blogPrompt}

)}

DID:

{did}

); }; export default Independent; ================================================ FILE: src/pages/AgentList/index.less ================================================ .agent-list { padding: 24px; box-sizing: border-box; min-height: 100vh; background: #141414; a { font-weight: 500; color: #f0b90b; text-decoration: inherit; } a:hover { color: #f0b90b; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; .logo { display: flex; align-items: center; gap: 8px; img { border-radius: 50%; } h1 { margin: 0; font-size: 24px; color: #ffffff; margin-right: 8px; } .github-link { display: flex; background-color: #282828; padding: 4px; align-items: center; gap: 4px; font-size: 14px; color: #999; } } } .agent-list-tabs { .ant-tabs-nav { margin-bottom: 0; } } } .agent-table { background: #1f1f1f; border-radius: 8px; .ant-table { background: transparent; .ant-table-thead > tr > th { background: #141414; color: rgba(255, 255, 255, 0.85); border-bottom: 1px solid #303030; } .ant-table-tbody > tr > td { background: transparent; border-bottom: 1px solid #303030; color: rgba(255, 255, 255, 0.85); } .ant-table-tbody > tr:hover > td { background: #262626; } .message-content { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .agent-name { display: flex; align-items: center; gap: 8px; } } .model-tag { font-size: 12px; background: rgba(0, 255, 173, 0.2); color: #f0b90b; padding: 2px 8px; border-radius: 4px; } } .ant-drawer { .ant-drawer-content { background: #1f1f1f; } .ant-drawer-header { background: #1f1f1f; border-bottom: 1px solid #303030; } .ant-drawer-title { color: #ffffff; } .ant-drawer-body { padding: 24px; } } @media (max-width: 768px) { .agent-list { padding: 16px; } .mobile-agent-list { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; .mobile-agent-list-loading { display: flex; justify-content: center; align-items: center; height: 60vh; } .mobile-agent-item { display: flex; flex-direction: column; gap: 8px; padding: 16px; background: #262626; border-radius: 8px; font-family: "NexaText-Regular"; .mobile-agent-item-label { font-family: "NexaText-Bold"; font-weight: 500; color: rgba(255, 255, 255, 0.85); } .mobile-agent-item-header { display: flex; align-items: center; gap: 8px; } .mobile-agent-item-avatar { width: 48px; height: 48px; border-radius: 50%; overflow: hidden; img { width: 100%; height: 100%; object-fit: cover; } } .mobile-agent-item-name { font-size: 16px; font-weight: 500; color: #ffffff; } .mobile-agent-item-description { font-size: 14px; color: rgba(255, 255, 255, 0.85); } .mobile-agent-item-did { font-size: 12px; color: rgba(255, 255, 255, 0.85); } .mobile-agent-item-ipfsHash { font-size: 12px; color: rgba(255, 255, 255, 0.85); cursor: pointer; a { color: #f0b90b; text-decoration: underline; } } .mobile-agent-item-action { display: flex; justify-content: flex-end; margin-top: 16px; button { width: 100%; } } } } } .top-agent-row { background-color: #242424 !important; &:hover > td { background-color: #242424 !important; } } ================================================ FILE: src/pages/AgentList/index.tsx ================================================ /** * AI Agents Marketplace component * Displays a list of AI agents and provides functionality to create new agents */ import React, { useState, useCallback, useEffect } from "react"; import { Avatar, Button, Table, Drawer, Space, message, Tooltip, Tabs, Spin, } from "antd"; import type { ColumnsType, TablePaginationConfig } from "antd/es/table"; import { WalletOutlined, MessageOutlined } from "@ant-design/icons"; import Publish from "../Publish"; import "./index.less"; import WalletConnect from "@/components/WalletConnect"; import logo from "@/assets/images/logo.jpg"; import { IRecord } from "@/services/upload"; import githubLogo from "@/assets/images/icon-github.png"; import xLogo from "@/assets/images/icon-X.png"; import AgentCard, { IContractHistoryRow } from "@/components/agentCard"; import MessageCard, { IMessageRow } from "@/components/MessageCard"; import { getMessageList } from "@/services/api"; import { AVATAR_URL, MESSAGE_URL } from "@/utils"; import avatar_default from "@/assets/images/default-avatar.png"; import icEthereum from "@/assets/images/ic-eth.png"; import icSolana from "@/assets/images/ic-sol.png"; import starPng from "@/assets/images/icon-star.png"; import { WalletService } from "@/services/wallet"; import { ENetwork } from "@/services/network"; import { networkState } from "@/store/network"; import { useRecoilState } from "recoil"; /** * Create authentication message with timestamp * @param address Wallet address * @returns Message and timestamp */ const createLoginMessage = (address: string) => { const timestamp = Math.floor(new Date().getTime() / 1000); const msg = ` Login on AIWS: This signature is used only for login and does not include any other fees. Wallet address: ${address} Nonce: ${timestamp} `; return { msg, timestamp, }; }; enum AgentListTab { Agents = "Agents", Messages = "Messages", } const MOBILE_BREAKPOINT = 768; const DID_BLACKLIST = [""]; const TOP_AGENTS = ["ainick.eth"]; /** * Main AgentList component */ const AgentList: React.FC = () => { // State management const [drawerOpen, setDrawerOpen] = useState(false); const [loading, setLoading] = useState(false); const [tableLoading, setTableLoading] = useState(false); const [tableMessageLoading, setTableMessageLoading] = useState(false); const [connecting, setConnecting] = useState(false); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [currentMessagePage, setCurrentMessagePage] = useState(1); const [pageSize, setPageSize] = useState(8); const [messagePageSize, setMessagePageSize] = useState(8); const [totalMessage, setTotalMessage] = useState(0); const [agents, setAgents] = useState([]); const [isMobile, setIsMobile] = useState(false); const [messages, setMessages] = useState([]); const [isWalletConnected, setIsWalletConnected] = useState(false); const [messageApi, contextHolder] = message.useMessage(); const [connectError, setConnectError] = useState(false); const [userCancelled, setUserCancelled] = useState(false); const walletService = WalletService.getInstance(); const [network, setNetwork] = useRecoilState(networkState); // Effects useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); checkMobile(); window.addEventListener("resize", checkMobile); return () => window.removeEventListener("resize", checkMobile); }, []); useEffect(() => { setIsWalletConnected(walletService.isConnected()); const unsubscribe = walletService.subscribe(() => { setIsWalletConnected(walletService.isConnected()); }); return unsubscribe; }, [walletService]); const fetchRecords = useCallback(async (): Promise => { try { setTableLoading(true); const { records } = await walletService.getAllRecords(); const latestRecords = records.reduce( (acc: { [key: string]: IRecord }, curr: IRecord) => { if (curr.did) { if (!acc[curr.did] || acc[curr.did].timestamp < curr.timestamp) { acc[curr.did] = curr; } } else { const key = `${curr.creator_address}-${curr.timestamp}`; acc[key] = curr; } return acc; }, {} ); const formattedAgents: IContractHistoryRow[] = Object.values( latestRecords as { [key: string]: IRecord } ) .filter((record) => !DID_BLACKLIST.includes(record.did)) .map((record: IRecord, index: number) => ({ id: `${record.creator_address}-${index}`, name: record.agent_name, avatar: record.avatar, timestamp: record.timestamp, description: record.agent_intro, did: record.did, ipfsHash: record.contenthash, address: record.creator_address, isTop: TOP_AGENTS.includes(record.did), network: record.network, })) .sort((a, b) => { if (a.isTop && !b.isTop) return -1; if (!a.isTop && b.isTop) return 1; return b.timestamp - a.timestamp; }); setTotal(formattedAgents.length); setAgents(formattedAgents); } catch (error) { console.error("Fetch records error:", error); // message.error("Failed to load agents"); } finally { setTableLoading(false); } }, [walletService]); const handleTableChange = (pagination: TablePaginationConfig) => { setCurrentPage(pagination.current || 1); setPageSize(pagination.pageSize || 10); }; /** * Handle wallet disconnection */ const handleDisconnect = useCallback(async () => { try { await walletService.disconnectWallet(); setUserCancelled(true); setIsWalletConnected(false); } catch (err) { localStorage.removeItem("Authentication-Tokens"); localStorage.removeItem("Token_address"); window.location.reload(); messageApi.error( err instanceof Error ? err.message : "failed to disconnect" ); } }, [walletService, messageApi]); const handleMessageTableChange = (pagination: TablePaginationConfig) => { setCurrentMessagePage(pagination.current || 1); setMessagePageSize(pagination.pageSize || 10); }; /** * Handle user login * @param address Wallet address * @returns Authentication token or undefined if failed */ const handleLogin = useCallback( async (address: string) => { if (loading) return; try { setLoading(true); const { msg } = createLoginMessage(address); const signature = await walletService.signMessage(msg); if (!signature) return; localStorage.setItem("Authentication-Tokens", signature); localStorage.setItem("Token_address", address); return signature; } catch (err) { const error = err as Error; if (error.message.includes("User rejected")) { await handleDisconnect(); setUserCancelled(true); } else { messageApi.error("login failed: " + error.message); } } finally { setLoading(false); } }, [loading, walletService, messageApi, handleDisconnect] ); /** * Handle wallet connection */ const handleConnect = useCallback(async () => { if (connecting) return; try { setConnecting(true); setConnectError(false); setUserCancelled(false); const savedType = network; await walletService.connectWallet(savedType); // after connection, if it is a new connection, trigger signature const walletInfo = walletService.getWalletInfo(); if (walletInfo?.address) { const storedAddress = localStorage.getItem("Token_address"); if (!storedAddress || storedAddress !== walletInfo.address) { await handleLogin(walletInfo.address); } } } catch (err) { const error = err as Error; setConnectError(true); if ( error.message.includes("User rejected") || error.message.includes("User denied") ) { setUserCancelled(true); localStorage.removeItem("Authentication-Tokens"); localStorage.removeItem("Token_address"); } } finally { setConnecting(false); } }, [connecting, walletService, handleLogin, network]); const fetchMessages = useCallback(async (): Promise => { try { setTableMessageLoading(true); const { data } = await getMessageList({ page: 1, limit: 100, }); const { list, total_count } = data; setMessages(list); setTotalMessage(total_count); } catch (error) { console.error("Fetch messages error:", error); // message.error("Failed to load messages"); } finally { setTableMessageLoading(false); } }, []); useEffect(() => { fetchRecords(); fetchMessages(); }, [fetchRecords, fetchMessages]); /** * Initialize login on component mount */ useEffect(() => { let mounted = true; const restoreWalletConnection = async () => { // check if there is stored login information const storedToken = localStorage.getItem("Authentication-Tokens"); const storedAddress = localStorage.getItem("Token_address"); // if there is no stored information, no need to restore if (!storedToken || !storedAddress || !network) { return; } try { // if the wallet is not connected, try to restore connection if (!isWalletConnected) { await walletService.connectWallet(network); return; } // if the wallet is connected but the address does not match, login again const walletInfo = walletService.getWalletInfo(); if (walletInfo?.address && walletInfo.address !== storedAddress) { const token = await handleLogin(walletInfo.address); if (token && mounted) { messageApi.success("connected successfully"); } } } catch (err) { const error = err as Error; // only handle errors that are not user cancelled if (!error.message.includes("User rejected") && mounted) { // clear all stored information localStorage.removeItem("Authentication-Tokens"); localStorage.removeItem("Token_address"); } } }; // only restore connection if not loaded and not cancelled if (!loading && !connectError && !userCancelled) { restoreWalletConnection(); } return () => { mounted = false; }; }, [ isWalletConnected, loading, connectError, userCancelled, walletService, handleLogin, messageApi, network, ]); /** * Show publish drawer if wallet is connected */ const showPublishDrawer = () => { if (!isWalletConnected) { message.error("please connect wallet"); return; } setDrawerOpen(true); }; /** * Handle creation of new agent * @param agent New agent data */ const handleCreateAgent = () => { setTimeout(() => { setDrawerOpen(false); fetchRecords(); }, 500); }; /** * Open chat with selected agent * @param record Record to handle */ const handleChat = (record: IContractHistoryRow) => { if (record.network === ENetwork.Ethereum) { window.open(`https://${record.did}.limo`, "_blank"); } else { window.open(`https://${record.did}.sol.build`, "_blank"); } }; /** * Table columns configuration */ const columns: ColumnsType = [ { title: "Agent", dataIndex: "name", key: "name", render: (_, record) => (
{record.name} {record.isTop && ( Top Agent )}
), }, { title: "Description", dataIndex: "description", key: "description", ellipsis: true, }, { title: "DID", dataIndex: "did", key: "did", render: (_, record) => record.network === ENetwork.Ethereum ? ( {record.did} ) : ( {record.did}.sol ), }, { title: "IPFS Hash", dataIndex: "ipfsHash", key: "ipfsHash", render: (_) => { return ( {_.slice(0, 6)}...{_.slice(-4)} ); }, }, { title: "Action", key: "action", render: (_, record) => ( ), }, ]; /** * Table columns configuration */ const messageColumns: ColumnsType = [ { title: "IPFS Hash", dataIndex: "message_cid ", key: "message_cid", render: (_, record) => ( {record.message_cid.slice(0, 6)}...{record.message_cid.slice(-4)} ), }, { title: "Time", dataIndex: "time", key: "time", render: (_, record) => { return ( {new Date(record.create_time * 1000).toLocaleString()} ); }, }, { title: "Content", dataIndex: "content", key: "content", render: (_, record) => (
{record.message}
), }, { title: "Sender", dataIndex: "sender", key: "sender", render: (_, record: IMessageRow) => ( {record.role === 1 ? ( <> {record.agent_files_info.agent_name} ) : ( <> User )} ), }, { title: "Previous IPFS Hash", dataIndex: "prev_message_cid", key: "prev_message_cid", render: (_, record) => record.prev_message_cid ? ( {record.prev_message_cid.slice(0, 6)}... {record.prev_message_cid.slice(-4)} ) : ( -- ), }, ]; return (
{contextHolder}

AIWS

{!isMobile && ( {/* */} { setNetwork(type); // after connection, trigger signature immediately const walletInfo = walletService.getWalletInfo(); if (walletInfo?.address) { const storedAddress = localStorage.getItem("Token_address"); if (!storedAddress || storedAddress !== walletInfo.address) { await handleLogin(walletInfo.address); } } }} /> )}
{!isMobile ? ( { setCurrentPage(page); setPageSize(size); }, }} onChange={handleTableChange} rowClassName={(record) => (record.isTop ? "top-agent-row" : "")} /> ) : (
{tableLoading ? (
) : ( agents.map((agent) => ( )) )}
)} {!isMobile ? (
{ setCurrentMessagePage(page); setMessagePageSize(size); }, }} onChange={handleMessageTableChange} /> ) : (
{tableMessageLoading ? (
) : ( messages.map((message) => ( )) )}
)} setDrawerOpen(false)} open={drawerOpen} width="100%" styles={{ body: { padding: 24, display: "flex", justifyContent: "center", }, }} > {isWalletConnected ? (
) : (

Please connect wallet first

)}
); }; export default AgentList; ================================================ FILE: src/pages/Publish/index.less ================================================ .publish-container { width: 100%; padding: 24px 10%; box-sizing: border-box; .publish-steps { .ant-steps-icon { color: #141414 !important; } } .publish-card { max-width: 800px; margin: 0 auto; .ant-upload-select { width: 128px !important; height: 128px !important; } .ant-form-item-label { font-weight: 500; } } .ant-progress { margin-top: 16px; } .ant-btn-loading { opacity: 0.8; } .sub-label { .ant-form-item-label > label { font-size: 14px !important; } } } #media-form { .ant-form-item-label > label { font-size: 14px !important; } } .media-modal { .ant-modal-title { font-size: 20px !important; } .ant-modal-close { top: 18px !important; } } .ant-form-item { .ant-input-textarea-show-count { &::after { color: rgba(0, 0, 0, 0.45); } } .ant-checkbox-inner:after { border-color: #333; } .config-desc { color: #4f4f4f; font-size: 14px; } .social-media-desc { color: #4f4f4f; font-size: 14px; margin-bottom: 8px; } .social-media-container { display: flex; gap: 10px; align-items: center; justify-content: space-between; .social-media-title { font-size: 12px; font-weight: 500; color: #fff; } .social-media-icon { width: 90px; display: flex; justify-content: center; align-items: center; flex-direction: column; gap: 4px; .connect-button { width: 90px; height: 24px; line-height: 24px; text-align: center; font-size: 12px; border-radius: 6px; color: #f0b90b; background-color: #141414; border: 1px solid rgba(255, 255, 255, 0.2); cursor: pointer; } } } .ant-form-item-label { label { width: 100%; font-size: 16px; &::after { content: ""; margin: 0; } } } .prompt-label { flex: 1; width: 100%; display: flex; justify-content: space-between; align-items: center; .optional-label { margin-left: 8px; color: #999; font-size: 16px; } a { color: #f0b90b; font-size: 14px; &:hover { text-decoration: underline; } } } .ant-checkbox-wrapper { margin-bottom: 4px; } } ================================================ FILE: src/pages/Publish/index.tsx ================================================ /* eslint-disable @typescript-eslint/no-explicit-any */ /** * AI Agent Publication Component * Allows users to create and publish new AI agents */ import React, { useState } from "react"; import { Form, Input, Upload, Button, message, Select, Spin, Steps, Checkbox, CheckboxChangeEvent, Modal, Tooltip, } from "antd"; import { UploadOutlined } from "@ant-design/icons"; import type { RcFile } from "antd/es/upload/interface"; import "./index.less"; import { uploadToIPFS } from "@/services/upload"; import { ENetwork } from "@/services/network"; import { PublishStep, StepData, FormValues } from "@/types"; import { WalletService } from "@/services/wallet"; import x from "@/assets/images/x.png"; import tg from "@/assets/images/tg.png"; import farcaster from "@/assets/images/farcaster.png"; import discord from "@/assets/images/discord.png"; import { useRecoilState } from "recoil"; import { isWalletConnectedState } from "@/store/network"; const { TextArea } = Input; /** * Props interface for Publish component */ interface PublishProps { onSuccess: () => void; } // Define step messages const STEPS = { PREPARING: "Preparing files...", CREATING_AGENT: "Creating Agent...", UPLOADING_FILES: "Uploading files...", CONFIRMING: "Binding Domain...", COMPLETED: "Completed!", }; const enum EDataset { INDEX3 = "Index3", FARCASTER = "Farcaster", WEB3NEWS = "Web3 News", DAILYFEEDS = "Dailyfeeds", COINGECKO = "Coingecko", KNOWLEDGEBASE = "KnowledgeBase", } // add social media type definition interface SocialMediaConfig { [ESocialMedia.TWITTER]?: { apiKey: string; apiSecret: string; accessToken: string; accessTokenSecret: string; userId: string; }; [ESocialMedia.TELEGRAM]?: { botToken: string; chatId: string; }; [ESocialMedia.FARCASER]?: { apiKey: string; username: string; }; [ESocialMedia.DISCORD]?: { webhookUrl: string; botName: string; }; } enum ESocialMedia { TWITTER = "twitter", TELEGRAM = "telegram", FARCASER = "farcaster", DISCORD = "discord", } /** * Publish component for creating new AI agents */ const Publish: React.FC = ({ onSuccess }) => { // Form and state management const [form] = Form.useForm(); const chatConfigValue = Form.useWatch("chatConfig", form); const blogConfigValue = Form.useWatch("blogConfig", form); const [avatarFile, setAvatarFile] = useState(); const [submitting, setSubmitting] = React.useState(false); const [currentStep, setCurrentStep] = useState( PublishStep.CONTRACT ); const [stepMessage, setStepMessage] = useState(""); const [domains, setDomains] = React.useState([]); const [loadingDomains, setLoadingDomains] = React.useState(false); const [imageUrl, setImageUrl] = useState(); const [stepData, setStepData] = useState({ step: PublishStep.CONTRACT, }); const walletService = WalletService.getInstance(); const address = walletService.getWalletInfo()?.address; const [isWalletConnected] = useRecoilState(isWalletConnectedState); const network = walletService.getCurrentNetwork(); const [errorStep, setErrorStep] = useState(null); const isFormDisabled = currentStep !== PublishStep.CONTRACT; // add social media config const [socialMediaConfig, setSocialMediaConfig] = useState( {} ); const [currentMedia, setCurrentMedia] = useState(null); const [mediaModalVisible, setMediaModalVisible] = useState(false); const [mediaForm] = Form.useForm(); // add dataset Form.useWatch const datasetValue = Form.useWatch("dataset", form); /** * Validate and upload avatar before adding to form */ const beforeUpload = (file: RcFile): boolean => { const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png"; if (!isJpgOrPng) { message.error("You can only upload JPG/PNG files!"); return false; } const isLt1M = file.size / 1024 / 1024 < 1; if (!isLt1M) { message.error("Image must be smaller than 1MB!"); return false; } setAvatarFile(file); getBase64(file, setImageUrl); return false; }; const getErrorMessage = (step: PublishStep) => { switch (step) { case PublishStep.CONTRACT: return "Contract creation failed. Please try this step again."; case PublishStep.IPFS: return "IPFS upload failed. Please try this step again."; case PublishStep.ENS: return "Domain record update failed. Please try this step again."; default: return "An error occurred. Please try again."; } }; // Handle contract step const handleContractStep = async (values: FormValues) => { try { setErrorStep(null); setSubmitting(true); setStepMessage(STEPS.PREPARING); if (!avatarFile) { message.error("Please upload an avatar!"); return; } const data = { name: values.name, avatar: avatarFile, functionDesc: values.functionDesc, behaviorDesc: values.behaviorDesc, did: values.did, dataset: values.dataset, blog_dataset: values.blog_dataset, blogPrompt: values.blogPrompt, hasBlog: values.blogPrompt ? true : false, hasRAG: values.chatConfig, website: values.website, website1: values.website1, website2: values.website2, }; setStepMessage(STEPS.CREATING_AGENT); const { txHash, ipfsInfo, fileList } = await walletService.createRecord( data ); // Save data const newStepData = { step: PublishStep.IPFS, formData: values, fileList, contractData: { txHash, }, ipfsData: { ...ipfsInfo, ipfsUploaded: false, }, }; setStepData(newStepData); // Go to next step updateStep(PublishStep.IPFS, newStepData); } catch (error) { console.error("Contract step failed:", error); message.error("Contract step failed"); setErrorStep(PublishStep.CONTRACT); setSubmitting(false); setStepMessage(""); } }; const handleIpfsStep = async (stepData: StepData) => { try { setErrorStep(null); setSubmitting(true); setStepMessage(STEPS.UPLOADING_FILES); const { formData, contractData, ipfsData } = stepData; if (!formData || !contractData || !ipfsData) { throw new Error("Missing required data"); } if (!avatarFile) { throw new Error("Avatar file not found"); } let chainId = "1"; if (network === ENetwork.Solana) { chainId = "101"; } const socialMediaData = socialMediaConfig[ESocialMedia.TWITTER] ? { twitter_user_id: socialMediaConfig[ESocialMedia.TWITTER].userId || "", twitter_client_secret: socialMediaConfig[ESocialMedia.TWITTER].apiSecret || "", twitter_api_key: socialMediaConfig[ESocialMedia.TWITTER].apiKey || "", twitter_api_secret: socialMediaConfig[ESocialMedia.TWITTER].apiSecret || "", twitter_access_token: socialMediaConfig[ESocialMedia.TWITTER].accessToken || "", twitter_access_secret: socialMediaConfig[ESocialMedia.TWITTER].accessTokenSecret || "", } : undefined; const result = await uploadToIPFS( stepData, chainId, socialMediaData, (percent: any) => { setStepMessage(`Uploading... ${percent}%`); } ); if (!result || !result.contentHash) { throw new Error("Upload failed"); } const newStepData = { ...stepData, ipfsData: { ...ipfsData, contentHash: result.contentHash, ipfsUploaded: true, }, }; setStepData(newStepData); updateStep(PublishStep.ENS, newStepData); } catch (error) { console.error("IPFS step failed:", error); message.error("IPFS upload failed"); setErrorStep(PublishStep.IPFS); setSubmitting(false); setStepMessage(""); } }; const handleEnsStep = async (stepData: StepData) => { try { setErrorStep(null); setSubmitting(true); setStepMessage(STEPS.CONFIRMING); const { formData, ipfsData } = stepData; if (!formData || !ipfsData || !ipfsData.ipfsUploaded) { throw new Error("Missing required data"); } // Set records try { await walletService.setRecord({ did: formData.did, contenthash: ipfsData.contentHash, }); message.success("Records updated successfully"); } catch (error) { console.error("Failed to set records:", error); if (error instanceof Error) { message.error(`Failed to update records: ${error.message}`); } else { message.error("Failed to update records"); } return; } updateStep(PublishStep.COMPLETED); setStepMessage(STEPS.COMPLETED); onSuccess(); message.success("Agent created successfully"); form.resetFields(); setAvatarFile(undefined); setImageUrl(undefined); setStepData({ step: PublishStep.CONTRACT }); setCurrentStep(PublishStep.CONTRACT); } catch (error) { console.error("failed:", error); message.error("Domain update failed"); setErrorStep(PublishStep.ENS); } finally { setSubmitting(false); setStepMessage(""); } }; const onFinish = async (values: FormValues) => { // add social media config to values const formValuesWithSocial = { ...values, socialMediaConfig, }; switch (currentStep) { case PublishStep.CONTRACT: await handleContractStep(formValuesWithSocial); break; case PublishStep.IPFS: await handleIpfsStep(stepData); break; case PublishStep.ENS: await handleEnsStep(stepData); break; default: break; } }; /** * Fetch ENS domains on component mount */ React.useEffect(() => { const fetchENSDomains = async () => { if (!address) return; try { setLoadingDomains(true); const ownedNames = await walletService.getAllOwnedDomains(); if (ownedNames.length > 0) { setDomains(ownedNames); } } catch (error) { console.error("Failed to fetch domains:", error); message.error("Failed to load domains. Please try again later."); } finally { setLoadingDomains(false); } }; fetchENSDomains(); }, [address, walletService]); const handleReset = () => { form.resetFields(); setAvatarFile(undefined); setImageUrl(undefined); setLoadingDomains(false); setStepData({ step: PublishStep.CONTRACT }); setCurrentStep(PublishStep.CONTRACT); setStepMessage(""); }; const uploadButton = (
Upload
); const getBase64 = (img: RcFile, callback: (url: string) => void) => { const reader = new FileReader(); reader.addEventListener("load", () => callback(reader.result as string)); reader.readAsDataURL(img); }; // Execute step logic const executeStep = async (step: PublishStep, stepData: StepData) => { switch (step) { case PublishStep.CONTRACT: await form.validateFields(); await handleContractStep(form.getFieldsValue()); break; case PublishStep.IPFS: await handleIpfsStep(stepData); break; case PublishStep.ENS: await handleEnsStep(stepData); break; default: break; } }; // Update current step const updateStep = (newStep: PublishStep, stepData?: StepData) => { setCurrentStep(newStep); if (newStep !== PublishStep.COMPLETED && stepData) { executeStep(newStep, stepData); } }; const handleChatConfigChange = (e: CheckboxChangeEvent) => { if (e.target.checked) { form.setFieldsValue({ chatConfig: true, dataset: undefined, blog_dataset: undefined, }); } else { form.setFieldsValue({ chatConfig: false, dataset: undefined, blog_dataset: undefined, }); } }; const handleBlogConfigChange = (e: CheckboxChangeEvent) => { if (e.target.checked) { form.setFieldsValue({ blogConfig: true, blogPrompt: undefined, }); } else { form.setFieldsValue({ blogConfig: false, blogPrompt: undefined, blog_dataset: undefined, }); } }; const initialValues = { chatConfig: false, blogConfig: false, dataset: undefined, blogPrompt: undefined, }; // add media icon component const SocialMediaIcon = ({ type, onClick, active, }: { type: ESocialMedia; onClick: () => void; active: boolean; }) => { const getIcon = () => { switch (type) { case ESocialMedia.TWITTER: return ( <> twitter

Twitter

); case ESocialMedia.TELEGRAM: return ( <> telegram

Telegram

); case ESocialMedia.FARCASER: return ( <> farcaster

Farcaster

); case ESocialMedia.DISCORD: return ( <> discord

Discord

); default: return null; } }; const getTooltipTitle = () => { switch (type) { case ESocialMedia.TWITTER: return "Twitter"; case ESocialMedia.TELEGRAM: return "Telegram"; case ESocialMedia.FARCASER: return "Farcaster"; case ESocialMedia.DISCORD: return "Discord"; default: return ""; } }; return (
{getIcon()}
); }; // add media form fields config const getMediaFormFields = (type: string) => { switch (type) { case ESocialMedia.TWITTER: return ( <> ); case ESocialMedia.TELEGRAM: return ( <> ); case ESocialMedia.FARCASER: return ( <> ); case ESocialMedia.DISCORD: return ( <> ); default: return null; } }; // add handle function const handleMediaIconClick = (type: ESocialMedia) => { setCurrentMedia(type); mediaForm.resetFields(); // if has config, fill form if (socialMediaConfig[type]) { mediaForm.setFieldsValue(socialMediaConfig[type]); } setMediaModalVisible(true); }; const handleMediaFormSubmit = () => { mediaForm.validateFields().then((values) => { setSocialMediaConfig((prev) => ({ ...prev, [currentMedia as string]: values, })); setMediaModalVisible(false); message.success(`${currentMedia} configuration saved`); }); }; return (
form={form} layout="vertical" onFinish={onFinish} requiredMark={false} onReset={handleReset} initialValues={initialValues} > {imageUrl ? ( avatar ) : ( uploadButton )}