From 14c38ec4dcd9bd5f294579d8e53ba0c1277eb8d5 Mon Sep 17 00:00:00 2001 From: liaozip Date: Fri, 3 Apr 2026 07:28:39 +0800 Subject: [PATCH 1/6] feat: enhance UI with i18n support and emoji beautification --- .trae/skills/emoji-title-beautifier/SKILL.md | 346 ++++++++ .trae/skills/i18n-helper/SKILL.md | 290 +++++++ frontend/src/components/AgentCredentials.tsx | 139 +-- frontend/src/components/ChannelConfig.tsx | 3 +- frontend/src/components/FileBrowser.tsx | 2 +- frontend/src/i18n/en.json | 755 +++++++++++++++- frontend/src/i18n/zh.json | 855 +++++++++++++++++-- frontend/src/pages/AdminCompanies.tsx | 61 +- frontend/src/pages/AgentCreate.tsx | 32 +- frontend/src/pages/AgentDetail.tsx | 197 ++--- frontend/src/pages/EnterpriseSettings.tsx | 64 +- frontend/src/pages/Layout.tsx | 4 +- frontend/src/pages/OpenClawSettings.tsx | 4 +- frontend/src/pages/PlatformDashboard.tsx | 115 +-- frontend/src/pages/UserManagement.tsx | 59 +- 15 files changed, 2488 insertions(+), 438 deletions(-) create mode 100644 .trae/skills/emoji-title-beautifier/SKILL.md create mode 100644 .trae/skills/i18n-helper/SKILL.md diff --git a/.trae/skills/emoji-title-beautifier/SKILL.md b/.trae/skills/emoji-title-beautifier/SKILL.md new file mode 100644 index 00000000..f0c2b667 --- /dev/null +++ b/.trae/skills/emoji-title-beautifier/SKILL.md @@ -0,0 +1,346 @@ +--- +name: "emoji-title-beautifier" +description: "Helps add emojis to UI titles and labels for visual enhancement. Invoke when user wants to beautify titles, add emojis, or improve UI visual appeal." +--- + +# Emoji Title Beautifier + +This skill provides guidelines and best practices for adding emojis to UI titles, labels, and other text elements to enhance visual appeal and user experience. + +## When to Add Emojis + +**Add emojis when:** +- Enhancing page titles and section headers +- Improving table column headers +- Adding visual indicators to navigation items +- Making labels more intuitive and recognizable +- Improving the visual hierarchy of UI elements + +**Avoid emojis when:** +- In formal or legal text +- In error messages that need to be clear and professional +- In places where screen readers might struggle +- When it could distract from critical information + +## Emoji Selection Guidelines + +### Category-Based Emoji Mapping + +#### Business & Organization +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 🏢 | Office Building | Company, organization, enterprise | +| 👥 | People | Users, team members, contacts | +| 👤 | Person | User profile, individual | +| 🤝 | Handshake | Partnership, agreement | + +#### Technology & Development +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 🤖 | Robot | AI agents, bots, automation | +| ⚙️ | Gear | Settings, configuration, operations | +| 🔧 | Wrench | Tools, maintenance, repair | +| 💻 | Computer | Development, coding, technical | +| 🔌 | Plug | Integration, connection, API | + +#### Security & Authentication +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 🔐 | Lock with Key | Security, authentication, SSO | +| 🔒 | Lock | Privacy, secure, protected | +| 🔑 | Key | Password, access, credentials | +| 🛡️ | Shield | Protection, security features | + +#### Communication +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 📧 | Email | Email address, messaging | +| 💬 | Speech Bubble | Chat, messages, comments | +| 📢 | Loudspeaker | Announcements, notifications | +| ✉️ | Envelope | Mail, invitations | + +#### Data & Analytics +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 📊 | Chart | Statistics, analytics, usage | +| 📈 | Increasing Chart | Growth, trends, progress | +| 📉 | Decreasing Chart | Decline, reduction | +| 📋 | Clipboard | Reports, lists, documents | + +#### Time & Status +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 📅 | Calendar | Date, schedule, time | +| ⏰ | Clock | Time, deadline, schedule | +| ⚡ | Lightning | Status, active, fast | +| ✅ | Check Mark | Completed, success, active | +| ❌ | Cross Mark | Disabled, failed, inactive | + +#### Content & Media +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 📝 | Memo | Notes, documentation, content | +| 📄 | Page | Documents, files | +| 📁 | Folder | Directory, file organization | +| 🖼️ | Frame | Images, media | + +#### Navigation & Actions +| Emoji | Meaning | Use Case | +|-------|---------|----------| +| 🏠 | House | Home, dashboard, main page | +| ➡️ | Arrow | Navigation, next, proceed | +| ⬆️ | Up Arrow | Upload, increase, move up | +| 🔄 | Counter-clockwise | Refresh, reset, sync | + +## Implementation Process + +### Step 1: Identify Text Elements + +Identify which UI elements need emoji enhancement: +- Page titles +- Section headers +- Table column headers +- Navigation items +- Form labels +- Button text (use sparingly) + +### Step 2: Choose Appropriate Emojis + +1. **Match the context**: Select emojis that accurately represent the content +2. **Consider cultural differences**: Some emojis may have different meanings in different cultures +3. **Maintain consistency**: Use the same emoji for the same concept throughout the application +4. **Keep it simple**: Don't overuse emojis; one per element is usually sufficient + +### Step 3: Add to Internationalization Files + +**In `frontend/src/i18n/en.json`:** +```json +{ + "moduleName": { + "title": "📊 Dashboard", + "users": "👥 Users", + "settings": "⚙️ Settings" + } +} +``` + +**In `frontend/src/i18n/zh.json`:** +```json +{ + "moduleName": { + "title": "📊 仪表盘", + "users": "👥 用户数", + "settings": "⚙️ 设置" + } +} +``` + +### Step 4: Verify Consistency + +Ensure both language files have the same emojis in corresponding keys. + +## Best Practices + +### 1. Consistency is Key +Use the same emoji for the same concept throughout the application: +- ✅ Good: Always use 🏢 for "Company" +- ❌ Bad: Use 🏢 in one place and 🏛️ in another + +### 2. Semantic Relevance +Choose emojis that semantically match the content: +- ✅ Good: 📧 for email addresses +- ❌ Bad: 📧 for phone numbers + +### 3. Visual Balance +Don't overcrowd the UI with emojis: +- Use emojis primarily for headers and labels +- Avoid emojis in body text or descriptions +- Keep button text clean (emojis optional) + +### 4. Accessibility Considerations +- Emojis should enhance, not replace text +- Ensure screen readers can still understand the content +- Provide text alternatives where necessary + +### 5. Professional Tone +Maintain professionalism: +- Use standard, widely-recognized emojis +- Avoid overly casual or playful emojis in business contexts +- Consider the target audience + +## Common Patterns + +### Navigation Tabs +```json +{ + "tab": { + "dashboard": "📊 Dashboard", + "platform": "⚙️ Platform", + "companies": "🏢 Companies" + } +} +``` + +### Table Headers +```json +{ + "table": { + "company": "🏢 Company", + "users": "👥 Users", + "agents": "🤖 Agents", + "status": "⚡ Status", + "action": "⚙️ Action" + } +} +``` + +### Form Labels +```json +{ + "form": { + "email": "📧 Email Address", + "password": "🔑 Password", + "name": "👤 Name" + } +} +``` + +### Status Indicators +```json +{ + "status": { + "active": "✅ Active", + "inactive": "❌ Inactive", + "pending": "⏳ Pending" + } +} +``` + +## Emoji Selection by Category + +### Dashboard & Analytics +- 📊 Dashboard, analytics, statistics +- 📈 Growth, increase, progress +- 📉 Decrease, decline +- 🎯 Goals, targets, objectives + +### User Management +- 👥 Users, team, group +- 👤 Individual user, profile +- 🔐 Authentication, security +- 🎫 Access, permissions + +### Content Management +- 📝 Notes, documentation +- 📄 Documents, files +- 📁 Folders, directories +- 🖼️ Images, media + +### Communication +- 📧 Email +- 💬 Chat, messages +- 📢 Announcements +- 🔔 Notifications + +### Settings & Configuration +- ⚙️ Settings, configuration +- 🔧 Tools, maintenance +- 🎨 Appearance, themes +- 🔌 Integrations, plugins + +### Status & Actions +- ✅ Success, completed, active +- ❌ Error, failed, inactive +- ⏳ Pending, loading +- 🔄 Refresh, sync, reset + +## Testing Checklist + +Before finalizing emoji additions: + +- [ ] Emojis are semantically appropriate +- [ ] Both language files have matching emojis +- [ ] Emojis render correctly in all target browsers +- [ ] Emojis don't break layout or alignment +- [ ] Screen readers can handle the content +- [ ] Emojis are consistent across the application +- [ ] Professional tone is maintained +- [ ] Visual hierarchy is improved, not cluttered + +## Examples from the Clawith Project + +### Admin Module +```json +{ + "admin": { + "tab": { + "dashboard": "📊 Dashboard", + "platform": "⚙️ Platform", + "companies": "🏢 Companies" + }, + "company": "🏢 Company", + "sso": "🔐 SSO", + "orgAdmin": "📧 Admin Email", + "users": "👥 Users", + "agents": "🤖 Agents", + "tokens": "📊 Token Usage", + "createdAt": "📅 Created", + "status": "⚡ Status", + "action": "⚙️ Action" + } +} +``` + +### Platform Dashboard +```json +{ + "platformDashboard": { + "metrics": { + "avgTokensPerSession": { + "label": "📊 Avg Tokens / Session" + }, + "retention7Day": { + "label": "📈 7-Day Retention" + } + } + } +} +``` + +## Troubleshooting + +### Issue: Emojis Not Displaying +**Solution**: +- Check browser compatibility +- Ensure UTF-8 encoding in HTML +- Verify JSON file encoding + +### Issue: Emojis Breaking Layout +**Solution**: +- Adjust column widths +- Use CSS to control emoji size +- Consider using emoji at the start of text only + +### Issue: Inconsistent Emojis +**Solution**: +- Create an emoji style guide +- Document emoji usage in comments +- Review all translations for consistency + +## Related Documentation + +- [Internationalization (i18n) Helper](/.trae/skills/i18n-helper/SKILL.md) +- [Email Templates I18N Guide](/DEV/docs/EMAIL_TEMPLATES_I18N.md) +- [Bug Fixes Log](/DEV/docs/BUG_FIXES.md) + +## Additional Resources + +- [Full Emoji List](https://unicode.org/emoji/charts/full-emoji-list.html) +- [Emojipedia](https://emojipedia.org/) +- [Emoji Design Guidelines](https://unicode.org/emoji/charts/emoji-design-guidelines.html) + +--- + +**Last Updated**: 2026-04-03 +**Version**: 1.0 +**Author**: Clawith Development Team diff --git a/.trae/skills/i18n-helper/SKILL.md b/.trae/skills/i18n-helper/SKILL.md new file mode 100644 index 00000000..c6fc5b2d --- /dev/null +++ b/.trae/skills/i18n-helper/SKILL.md @@ -0,0 +1,290 @@ +--- +name: "i18n-helper" +description: "Helps add and manage internationalization (i18n) for the Clawith project. Invoke when adding new UI text, translating content, or working with i18n files." +--- + +# Internationalization (i18n) Helper + +This skill provides guidelines and best practices for adding internationalization support to the Clawith project. + +## Project i18n Structure + +The Clawith project uses **i18next** for internationalization with the following structure: + +``` +frontend/src/i18n/ +├── en.json # English translations +├── zh.json # Chinese translations +└── index.ts # i18n configuration +``` + +## When to Add i18n + +**ALWAYS add internationalization when:** +- Adding new UI text (buttons, labels, titles, descriptions) +- Creating new pages or components with text content +- Adding error messages or notifications +- Creating tooltips, placeholders, or help text +- Adding table headers or column names + +## Step-by-Step Process + +### 1. Add Translation Keys + +**In `frontend/src/i18n/en.json`:** +```json +{ + "moduleName": { + "sectionName": { + "title": "Title in English", + "description": "Description text", + "button": { + "save": "Save", + "cancel": "Cancel" + } + } + } +} +``` + +**In `frontend/src/i18n/zh.json`:** +```json +{ + "moduleName": { + "sectionName": { + "title": "中文标题", + "description": "描述文本", + "button": { + "save": "保存", + "cancel": "取消" + } + } + } +} +``` + +### 2. Use Translations in Components + +**Import the translation hook:** +```tsx +import { useTranslation } from 'react-i18next'; + +function MyComponent() { + const { t } = useTranslation(); + + return ( +
+

{t('moduleName.sectionName.title')}

+

{t('moduleName.sectionName.description')}

+ +
+ ); +} +``` + +**With fallback text:** +```tsx +{t('moduleName.key', 'Default English Text')} +``` + +## Naming Conventions + +### Module Structure +Organize translations by feature/module: +- `common` - Shared UI elements (buttons, labels, messages) +- `admin` - Admin panel +- `agent` - Agent-related features +- `company` - Company management +- `enterprise` - Enterprise settings +- `platformDashboard` - Platform dashboard + +### Key Naming Rules +1. **Use camelCase** for keys: `userName`, `createAgent` +2. **Be descriptive**: `button.save` not `btn.sv` +3. **Group related keys**: + ```json + "button": { "save": "...", "cancel": "..." } + ``` +4. **Use consistent suffixes**: + - `.title` - Page/section titles + - `.description` - Descriptive text + - `.placeholder` - Input placeholders + - `.tooltip` - Tooltip text + - `.label` - Form labels + - `.message` - Status/error messages + +## Best Practices + +### 1. Always Provide Both Languages +Never add only English or only Chinese. Both `en.json` and `zh.json` must have the same keys. + +### 2. Use Interpolation for Dynamic Content +```json +{ + "welcome": "Welcome, {{name}}!", + "itemsCount": "{{count}} items found" +} +``` + +```tsx +t('welcome', { name: userName }) +t('itemsCount', { count: items.length }) +``` + +### 3. Keep Translations Organized +- Group by module/feature +- Maintain alphabetical order within groups +- Use nested structure for related keys + +### 4. Avoid Hard-coded Text +❌ **Bad:** +```tsx + +``` + +✅ **Good:** +```tsx + +``` + +### 5. Use Fallback Text for New Features +```tsx +{t('newFeature.title', 'New Feature Title')} +``` + +## Common Patterns + +### Page Titles and Subtitles +```json +{ + "admin": { + "platformSettings": "Platform Settings", + "platformSettingsDesc": "Manage platform-wide settings and company tenants." + } +} +``` + +### Form Labels and Placeholders +```json +{ + "form": { + "email": { + "label": "Email Address", + "placeholder": "Enter your email" + } + } +} +``` + +### Error Messages +```json +{ + "error": { + "required": "This field is required", + "invalidEmail": "Please enter a valid email address" + } +} +``` + +### Success Messages +```json +{ + "success": { + "saved": "Changes saved successfully", + "created": "{{item}} created successfully" + } +} +``` + +## Validation Checklist + +Before committing i18n changes, verify: + +- [ ] Both `en.json` and `zh.json` have the same keys +- [ ] JSON syntax is valid (no trailing commas, proper quotes) +- [ ] Keys follow naming conventions (camelCase, descriptive) +- [ ] Translations are accurate and contextually appropriate +- [ ] No hard-coded text remains in components +- [ ] Interpolation variables are used correctly +- [ ] Fallback text is provided for new features + +## Tools and Utilities + +### Check for Missing Translations +```bash +# Compare keys between en.json and zh.json +diff <(jq -S 'keys' frontend/src/i18n/en.json) <(jq -S 'keys' frontend/src/i18n/zh.json) +``` + +### Validate JSON Syntax +```bash +# Check JSON syntax +jq . frontend/src/i18n/en.json > /dev/null && echo "en.json is valid" +jq . frontend/src/i18n/zh.json > /dev/null && echo "zh.json is valid" +``` + +## Examples from the Project + +### Platform Dashboard +```json +{ + "platformDashboard": { + "timeRange": { + "last7Days": "Last 7 Days", + "last30Days": "Last 30 Days" + }, + "metrics": { + "avgTokensPerSession": { + "label": "Avg Tokens / Session", + "tooltip": "Average token consumption per chat session..." + } + } + } +} +``` + +### Admin Module +```json +{ + "admin": { + "tab": { + "dashboard": "Dashboard", + "platform": "Platform", + "companies": "Companies" + }, + "platformSettings": "Platform Settings", + "platformSettingsDesc": "Manage platform-wide settings..." + } +} +``` + +## Troubleshooting + +### Translation Not Showing +1. Check if the key exists in both `en.json` and `zh.json` +2. Verify the key path matches exactly +3. Ensure JSON syntax is valid +4. Check browser console for i18n errors + +### Language Not Switching +1. Verify language files are properly loaded +2. Check i18n configuration in `frontend/src/i18n/index.ts` +3. Ensure language selector is working correctly + +### Missing Interpolation +1. Check that variables are passed to `t()` function +2. Verify variable names match in translation string +3. Example: `t('greeting', { name: 'John' })` for `"greeting": "Hello {{name}}"` + +## Related Files + +- `frontend/src/i18n/index.ts` - i18n configuration +- `frontend/src/i18n/en.json` - English translations +- `frontend/src/i18n/zh.json` - Chinese translations +- `.agents/rules/design_and_dev.md` - Development guidelines + +## Additional Resources + +- [i18next Documentation](https://www.i18next.com/) +- [React i18next](https://react.i18next.com/) +- Project Architecture: `ARCHITECTURE_SPEC_EN.md` diff --git a/frontend/src/components/AgentCredentials.tsx b/frontend/src/components/AgentCredentials.tsx index 7a4d2b1a..7864521e 100644 --- a/frontend/src/components/AgentCredentials.tsx +++ b/frontend/src/components/AgentCredentials.tsx @@ -51,13 +51,6 @@ const EMPTY_FORM: FormData = { cookies_json: '', }; -/* ── Status badge styles ── */ -const statusColors: Record = { - active: { bg: 'rgba(52, 199, 89, 0.12)', text: '#34c759', label: 'Active' }, - expired: { bg: 'rgba(255, 149, 0, 0.12)', text: '#ff9500', label: 'Expired' }, - needs_relogin: { bg: 'rgba(255, 59, 48, 0.12)', text: '#ff3b30', label: 'Needs Re-login' }, -}; - /* ── Icons ── */ const PlusIcon = ( @@ -99,19 +92,6 @@ const CookieIcon = ( ); -/* ── Relative time helper ── */ -function timeAgo(dateStr: string | null): string { - if (!dateStr) return ''; - const diff = Date.now() - new Date(dateStr).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return 'just now'; - if (mins < 60) return `${mins}m ago`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; - const days = Math.floor(hours / 24); - return `${days}d ago`; -} - /* ── Component ── */ interface Props { agentId: string; @@ -133,17 +113,40 @@ export default function AgentCredentials({ agentId }: Props) { // Delete confirmation const [deletingId, setDeletingId] = useState(null); + // Status badge styles - using translation keys + const getStatusConfig = useCallback((status: string) => { + const configs: Record = { + active: { bg: 'rgba(52, 199, 89, 0.12)', text: '#34c759', labelKey: 'agent.credentials.status.active' }, + expired: { bg: 'rgba(255, 149, 0, 0.12)', text: '#ff9500', labelKey: 'agent.credentials.status.expired' }, + needs_relogin: { bg: 'rgba(255, 59, 48, 0.12)', text: '#ff3b30', labelKey: 'agent.credentials.status.needs_relogin' }, + }; + return configs[status] || configs.active; + }, []); + + // Relative time helper using translations + const timeAgo = useCallback((dateStr: string | null): string => { + if (!dateStr) return ''; + const diff = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return t('agent.credentials.timeAgo.justNow'); + if (mins < 60) return t('agent.credentials.timeAgo.minutes', { count: mins }); + const hours = Math.floor(mins / 60); + if (hours < 24) return t('agent.credentials.timeAgo.hours', { count: hours }); + const days = Math.floor(hours / 24); + return t('agent.credentials.timeAgo.days', { count: days }); + }, [t]); + const fetchCredentials = useCallback(async () => { try { setLoading(true); const data = await credentialApi.list(agentId); setCredentials(data); } catch (e: any) { - setError(e.message || 'Failed to load credentials'); + setError(e.message || t('agent.credentials.error')); } finally { setLoading(false); } - }, [agentId]); + }, [agentId, t]); useEffect(() => { fetchCredentials(); @@ -173,7 +176,7 @@ export default function AgentCredentials({ agentId }: Props) { const handleSave = async () => { if (!form.platform.trim()) { - setFormError('Platform is required'); + setFormError(t('agent.credentials.platformRequired')); return; } @@ -182,11 +185,11 @@ export default function AgentCredentials({ agentId }: Props) { try { const parsed = JSON.parse(form.cookies_json); if (!Array.isArray(parsed)) { - setFormError('Cookies must be a JSON array'); + setFormError(t('agent.credentials.cookiesInvalid')); return; } } catch { - setFormError('Invalid JSON format for cookies'); + setFormError(t('agent.credentials.cookiesJsonInvalid')); return; } } @@ -215,7 +218,7 @@ export default function AgentCredentials({ agentId }: Props) { setShowModal(false); await fetchCredentials(); } catch (e: any) { - setFormError(e.message || 'Save failed'); + setFormError(e.message || t('agent.credentials.saveError')); } finally { setSaving(false); } @@ -227,7 +230,7 @@ export default function AgentCredentials({ agentId }: Props) { setDeletingId(null); await fetchCredentials(); } catch (e: any) { - setError(e.message || 'Delete failed'); + setError(e.message || t('agent.credentials.deleteError')); } }; @@ -237,20 +240,18 @@ export default function AgentCredentials({ agentId }: Props) {
{KeyIcon} - {t('agent.credentials.title', 'Credentials')} + {t('agent.credentials.title')} {credentials.length}
{/* Description */}

- {t('agent.credentials.description', - 'Store login credentials and cookies for websites. Cookies are automatically injected when the agent opens a browser via AgentBay.' - )} + {t('agent.credentials.description')}

{/* Error */} @@ -258,16 +259,16 @@ export default function AgentCredentials({ agentId }: Props) { {/* Credential list */} {loading ? ( -
Loading...
+
{t('agent.credentials.loading')}
) : credentials.length === 0 ? (
{KeyIcon} - {t('agent.credentials.empty', 'No credentials configured')} + {t('agent.credentials.empty')}
) : (
{credentials.map((cred) => { - const statusInfo = statusColors[cred.status] || statusColors.active; + const statusConfig = getStatusConfig(cred.status); return (
@@ -276,9 +277,9 @@ export default function AgentCredentials({ agentId }: Props) {
- {statusInfo.label} + {t(statusConfig.labelKey)}
{cred.display_name && ( @@ -288,7 +289,7 @@ export default function AgentCredentials({ agentId }: Props) { {cred.has_cookies && ( {CookieIcon} - Cookies {cred.cookies_updated_at ? `(${timeAgo(cred.cookies_updated_at)})` : ''} + {t('agent.credentials.meta.cookies')} {cred.cookies_updated_at ? `(${timeAgo(cred.cookies_updated_at)})` : ''} )} {cred.username && ( @@ -298,7 +299,7 @@ export default function AgentCredentials({ agentId }: Props) { )} {cred.last_injected_at && ( - Injected {timeAgo(cred.last_injected_at)} + {t('agent.credentials.meta.injected')} {timeAgo(cred.last_injected_at)} )}
@@ -306,14 +307,14 @@ export default function AgentCredentials({ agentId }: Props) { @@ -322,14 +323,14 @@ export default function AgentCredentials({ agentId }: Props) { {/* Delete confirmation */} {deletingId === cred.id && (
- Delete credential for {cred.platform}? + {t('agent.credentials.deleteConfirm.title', { platform: cred.platform })}
- +
@@ -345,7 +346,7 @@ export default function AgentCredentials({ agentId }: Props) {
setShowModal(false)}>
e.stopPropagation()}>
-

{editingId ? 'Edit Credential' : 'Add Credential'}

+

{editingId ? t('agent.credentials.modal.editTitle') : t('agent.credentials.modal.addTitle')}

@@ -355,104 +356,104 @@ export default function AgentCredentials({ agentId }: Props) { {formError &&
{formError}
}