From ac23254fb8bcc432507348c9a9e49591bbbbede2 Mon Sep 17 00:00:00 2001 From: Bill Chirico Date: Wed, 4 Mar 2026 16:22:01 -0500 Subject: [PATCH 01/21] Create maintain-docs.md Signed-off-by: Bill Chirico --- .github/workflows/maintain-docs.md | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/maintain-docs.md diff --git a/.github/workflows/maintain-docs.md b/.github/workflows/maintain-docs.md new file mode 100644 index 00000000..92f106d5 --- /dev/null +++ b/.github/workflows/maintain-docs.md @@ -0,0 +1,84 @@ +--- +on: + schedule: + - cron: '0 9 * * *' # 4 AM EST = 9 AM UTC + workflow_dispatch: {} + +permissions: + contents: true + pull-requests: true + issues: true + +tools: + github: + edit: + +engine: copilot + +--- + +# Maintain AGENTS.md Documentation + +## Purpose + +Keep the AGENTS.md file accurate and current by: +- Reviewing merged pull requests since last run +- Checking updated source files (src/, web/, tests/, etc.) +- Updating AGENTS.md to reflect any architectural or pattern changes +- Creating a pull request if updates are needed + +## Instructions for the Agent + +1. **Fetch Recent Changes**: Query the repository for merged PRs and updated files from the past 24 hours + +2. **Review Architecture Changes**: Check if any of these directories have significant changes: + - `src/modules/` - New modules or modified patterns + - `src/api/` - API route or middleware changes + - `src/commands/` - New slash commands + - `src/utils/` - Utility additions or pattern changes + - `web/` - Dashboard updates + - `tests/` - Testing patterns + +3. **Analyze Merged PRs**: Look at PR titles and descriptions to identify: + - New features added + - Architecture decisions + - Pattern changes + - Testing approach changes + - Breaking changes + +4. **Update AGENTS.md if Needed**: + - Architecture Overview section: Add new modules or directories + - Key Patterns section: Document new patterns or changes + - Common Tasks section: Update task examples if workflows changed + - Resources section: Add new links if applicable + +5. **Create Pull Request**: If changes are needed: + - Create a branch named `docs/maintain-docs-2026-03-04` + - Update AGENTS.md with discovered changes + - Create a PR with: + - Title: "docs: update AGENTS.md from merged PRs and source changes" + - Description: List the changes reviewed and what was updated + - Label: `documentation` + - Auto-merge enabled if all checks pass + +6. **Quality Checks**: + - Ensure markdown formatting is correct + - Verify all links and references are accurate + - Check that code examples match current patterns + - Ensure sections remain organized and readable + +7. **If No Changes Needed**: Close silently or note in logs that AGENTS.md is current + +## Context + +AGENTS.md documents: +- Code quality standards (ESM, single quotes, semicolons, 2-space indent, Winston logger) +- Architecture overview (src/, web/ structure) +- Key patterns (config system, caching, AI integration, database) +- Common tasks (adding features, commands, API endpoints) +- Testing requirements (80% coverage) +- Git workflow and review bots +- Troubleshooting guides +- Resources + +Always maintain accuracy and completeness of this documentation file. From d13361c55e655a165205903f626a80556b352b48 Mon Sep 17 00:00:00 2001 From: Bill Chirico Date: Wed, 4 Mar 2026 19:16:28 -0500 Subject: [PATCH 02/21] refactor(web): replace channel/role ID inputs with selectors (#242) * refactor(web): replace channel/role ID inputs with selectors * fix(web): align alertChannelId type with usage * fix(ci,web): handle missing Railway token and normalize cleared channel values * feat(bot): support API-only mode for Railway preview environments * fix: address PR #242 review issues and Railway preview behavior * Delete .github/workflows/railway-preview.yml Signed-off-by: Bill Chirico * chore(pr242): remove railway preview changes from selector refactor * chore(pr242): remove railway workflow deletion from PR --------- Signed-off-by: Bill Chirico Co-authored-by: Bill Co-authored-by: Bill Chirico --- .../dashboard/config-sections/ModerationSection.tsx | 10 ++++++---- web/src/types/config.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/web/src/components/dashboard/config-sections/ModerationSection.tsx b/web/src/components/dashboard/config-sections/ModerationSection.tsx index 86764362..39283df4 100644 --- a/web/src/components/dashboard/config-sections/ModerationSection.tsx +++ b/web/src/components/dashboard/config-sections/ModerationSection.tsx @@ -2,7 +2,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { ChannelSelector } from '@/components/ui/channel-selector'; -import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RoleSelector } from '@/components/ui/role-selector'; import { Switch } from '@/components/ui/switch'; @@ -183,17 +182,20 @@ export function ModerationSection({ />
-
- {/* Undo last save — visible only after a successful save with no new changes */} {prevSavedConfig && !hasChanges && (
- onFieldChange('enabled', v)} + onFieldChange('enabled', v)} disabled={saving} - aria-label="Toggle AI Auto-Moderation" + label="AI Auto-Moderation" /> - - {/* Model */} -
- - -
- - {/* Flag channel */} -
- -

- Flagged messages are posted here for manual review. -

- {guildId ? ( - - ) : ( -

Select a server first

- )} -
- - {/* Auto-delete */} + +
-
- -

- Delete the offending message before taking action. -

-
- onFieldChange('autoDelete', v)} - disabled={saving || !enabled} - aria-label="Toggle auto-delete" + Auto-delete flagged messages + onFieldChange('autoDelete', v)} + disabled={saving} + label="Auto-delete" />
- - {/* Per-category thresholds and actions */} -
- +
+ Thresholds (0–100)

- Set the confidence threshold (0–100%) and action for each category. + Confidence threshold (%) above which the action triggers.

- {categories.map(({ key, label, description }) => ( -
-
-

{label}

-

{description}

-
-
-
-
- Threshold - {Math.round((thresholds[key] ?? 0.7) * 100)}% -
- handleThresholdChange(key, v)} - disabled={saving || !enabled} - /> -
-
- -
-
-
+ {(['toxicity', 'spam', 'harassment'] as const).map((cat) => ( + ))} -
+ +
+ Actions + {(['toxicity', 'spam', 'harassment'] as const).map((cat) => ( + + ))} +
); diff --git a/web/src/components/dashboard/config-sections/AiSection.tsx b/web/src/components/dashboard/config-sections/AiSection.tsx index c0b27157..1e177f28 100644 --- a/web/src/components/dashboard/config-sections/AiSection.tsx +++ b/web/src/components/dashboard/config-sections/AiSection.tsx @@ -1,27 +1,38 @@ 'use client'; import { SystemPromptEditor } from '@/components/dashboard/system-prompt-editor'; -import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { ChannelSelector } from '@/components/ui/channel-selector'; import type { GuildConfig } from '@/lib/config-utils'; import { SYSTEM_PROMPT_MAX_LENGTH } from '@/types/config'; +import { ToggleSwitch } from '../toggle-switch'; interface AiSectionProps { draftConfig: GuildConfig; + guildId: string; saving: boolean; onEnabledChange: (enabled: boolean) => void; onSystemPromptChange: (value: string) => void; + onBlockedChannelsChange: (channels: string[]) => void; } +/** + * AI Chat configuration section. + * + * Provides controls for enabling/disabling AI chat, editing the system prompt, + * and selecting blocked channels where the AI will not respond. + */ export function AiSection({ draftConfig, + guildId, saving, onEnabledChange, onSystemPromptChange, + onBlockedChannelsChange, }: AiSectionProps) { return ( <> + {/* AI section */}
@@ -29,28 +40,48 @@ export function AiSection({ AI Chat Configure the AI assistant behavior.
-
- - -
+
+ {/* System Prompt */} + + {/* AI Blocked Channels */} + + + Blocked Channels + + The AI will not respond in these channels (or their threads). + + + + {guildId ? ( + + ) : ( +

Select a server first

+ )} +
+
); } diff --git a/web/src/components/dashboard/config-sections/ChallengesSection.tsx b/web/src/components/dashboard/config-sections/ChallengesSection.tsx new file mode 100644 index 00000000..bf681290 --- /dev/null +++ b/web/src/components/dashboard/config-sections/ChallengesSection.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { Card, CardContent, CardTitle } from '@/components/ui/card'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface ChallengesSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onEnabledChange: (enabled: boolean) => void; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * Daily Coding Challenges configuration section. + * + * Provides controls for auto-posting daily coding challenges with hint and solve tracking. + */ +export function ChallengesSection({ + draftConfig, + saving, + onEnabledChange, + onFieldChange, +}: ChallengesSectionProps) { + return ( + + +
+ Daily Coding Challenges + +
+

+ Auto-post a daily coding challenge with hint and solve tracking. +

+
+ + + +
+
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/CommunityFeaturesSection.tsx b/web/src/components/dashboard/config-sections/CommunityFeaturesSection.tsx new file mode 100644 index 00000000..e66fad80 --- /dev/null +++ b/web/src/components/dashboard/config-sections/CommunityFeaturesSection.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { Card, CardContent, CardTitle } from '@/components/ui/card'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface CommunityFeaturesSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onToggleChange: (key: string, enabled: boolean) => void; +} + +const COMMUNITY_FEATURES = [ + { key: 'help', label: 'Help / FAQ', desc: '/help command for server knowledge base' }, + { key: 'announce', label: 'Announcements', desc: '/announce for scheduled messages' }, + { key: 'snippet', label: 'Code Snippets', desc: '/snippet for saving and sharing code' }, + { key: 'poll', label: 'Polls', desc: '/poll for community voting' }, + { + key: 'showcase', + label: 'Project Showcase', + desc: '/showcase to submit, browse, and upvote projects', + }, + { + key: 'review', + label: 'Code Reviews', + desc: '/review peer code review requests with claim workflow', + }, + { key: 'tldr', label: 'TL;DR Summaries', desc: '/tldr for AI channel summaries' }, + { key: 'afk', label: 'AFK System', desc: '/afk auto-respond when members are away' }, + { + key: 'engagement', + label: 'Engagement Tracking', + desc: '/profile stats — messages, reactions, days active', + }, +] as const; + +/** + * Community Features configuration section. + * + * Provides toggles for enabling/disabling various community commands per guild. + */ +export function CommunityFeaturesSection({ + draftConfig, + saving, + onToggleChange, +}: CommunityFeaturesSectionProps) { + return ( + + +
+ Community Features +
+

+ Enable or disable community commands per guild. +

+ {COMMUNITY_FEATURES.map(({ key, label, desc }) => ( +
+
+ {label} +

{desc}

+
+ onToggleChange(key, v)} + disabled={saving} + label={label} + /> +
+ ))} +
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/EngagementSection.tsx b/web/src/components/dashboard/config-sections/EngagementSection.tsx new file mode 100644 index 00000000..a3f2fe71 --- /dev/null +++ b/web/src/components/dashboard/config-sections/EngagementSection.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import type { GuildConfig } from '@/lib/config-utils'; + +interface EngagementSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onActivityBadgesChange: (badges: Array<{ days?: number; label?: string }>) => void; +} + +const DEFAULT_ACTIVITY_BADGES = [ + { days: 90, label: '👑 Legend' }, + { days: 30, label: '🌳 Veteran' }, + { days: 7, label: '🌿 Regular' }, + { days: 0, label: '🌱 Newcomer' }, +] as const; + +/** + * Engagement / Activity Badges configuration section. + * + * Provides controls for configuring badge tiers shown on /profile. + */ +export function EngagementSection({ + draftConfig, + saving, + onActivityBadgesChange, +}: EngagementSectionProps) { + const badges = draftConfig.engagement?.activityBadges ?? [...DEFAULT_ACTIVITY_BADGES]; + + return ( + + + Activity Badges +

+ Configure the badge tiers shown on /profile. Each badge requires a minimum number of + active days. +

+ {badges.map((badge, i) => ( +
+ { + const newBadges = [...badges]; + newBadges[i] = { + ...newBadges[i], + days: Math.max(0, parseInt(e.target.value, 10) || 0), + }; + onActivityBadgesChange(newBadges); + }} + disabled={saving} + /> + days → + { + const newBadges = [...badges]; + newBadges[i] = { ...newBadges[i], label: e.target.value }; + onActivityBadgesChange(newBadges); + }} + disabled={saving} + /> + +
+ ))} + +
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/GitHubSection.tsx b/web/src/components/dashboard/config-sections/GitHubSection.tsx new file mode 100644 index 00000000..bc1e6431 --- /dev/null +++ b/web/src/components/dashboard/config-sections/GitHubSection.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { Card, CardContent, CardTitle } from '@/components/ui/card'; +import { parseNumberInput } from '@/lib/config-normalization'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface GitHubSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * GitHub Activity Feed configuration section. + * + * Provides controls for GitHub feed channel and polling interval. + */ +export function GitHubSection({ draftConfig, saving, onFieldChange }: GitHubSectionProps) { + const feed = draftConfig.github?.feed ?? {}; + + return ( + + +
+ GitHub Activity Feed + onFieldChange('enabled', v)} + disabled={saving} + label="GitHub Feed" + /> +
+
+ + +
+
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/MemorySection.tsx b/web/src/components/dashboard/config-sections/MemorySection.tsx new file mode 100644 index 00000000..6a359778 --- /dev/null +++ b/web/src/components/dashboard/config-sections/MemorySection.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { parseNumberInput } from '@/lib/config-normalization'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface MemorySectionProps { + draftConfig: GuildConfig; + saving: boolean; + onEnabledChange: (enabled: boolean) => void; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * Memory configuration section. + * + * Provides controls for AI context memory and auto-extraction settings. + */ +export function MemorySection({ + draftConfig, + saving, + onEnabledChange, + onFieldChange, +}: MemorySectionProps) { + return ( + + +
+
+ Memory + Configure AI context memory and auto-extraction. +
+ +
+
+ + +
+ Auto-Extract + onFieldChange('autoExtract', v)} + disabled={saving} + label="Auto-Extract" + /> +
+
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/ModerationSection.tsx b/web/src/components/dashboard/config-sections/ModerationSection.tsx index 39283df4..a11e687f 100644 --- a/web/src/components/dashboard/config-sections/ModerationSection.tsx +++ b/web/src/components/dashboard/config-sections/ModerationSection.tsx @@ -1,24 +1,32 @@ 'use client'; +import { useEffect, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { ChannelSelector } from '@/components/ui/channel-selector'; -import { Label } from '@/components/ui/label'; -import { RoleSelector } from '@/components/ui/role-selector'; -import { Switch } from '@/components/ui/switch'; -import { useGuildSelection } from '@/hooks/use-guild-selection'; +import { Input } from '@/components/ui/input'; +import { parseNumberInput } from '@/lib/config-normalization'; import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; interface ModerationSectionProps { draftConfig: GuildConfig; + guildId: string; saving: boolean; + protectRoleIdsRaw: string; onEnabledChange: (enabled: boolean) => void; onFieldChange: (field: string, value: unknown) => void; onDmNotificationChange: (action: string, value: boolean) => void; onEscalationChange: (enabled: boolean) => void; + onRateLimitChange: (field: string, value: unknown) => void; + onLinkFilterChange: (field: string, value: unknown) => void; onProtectRolesChange: (field: string, value: unknown) => void; onWarningsChange?: (field: string, value: unknown) => void; } +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + /** * Render the Moderation settings card with controls for alert channel, auto-delete, DM notifications, escalation, protected roles, and the warning system. * @@ -34,15 +42,28 @@ interface ModerationSectionProps { */ export function ModerationSection({ draftConfig, + guildId, saving, + protectRoleIdsRaw, onEnabledChange, onFieldChange, onDmNotificationChange, onEscalationChange, + onRateLimitChange, + onLinkFilterChange, onProtectRolesChange, onWarningsChange, }: ModerationSectionProps) { - const guildId = useGuildSelection(); + // Local state for blocked domains raw input (parsed on blur) + // Must be before early return to satisfy React hooks rules + const blockedDomainsDisplay = (draftConfig.moderation?.linkFilter?.blockedDomains ?? []).join( + ', ', + ); + const [blockedDomainsRaw, setBlockedDomainsRaw] = useState(blockedDomainsDisplay); + useEffect(() => { + setBlockedDomainsRaw(blockedDomainsDisplay); + }, [blockedDomainsDisplay]); + if (!draftConfig.moderation) return null; const alertChannelId = draftConfig.moderation?.alertChannelId ?? ''; @@ -62,17 +83,17 @@ export function ModerationSection({ Configure moderation, escalation, and logging settings. -
- + Alert Channel {guildId ? (
- - Auto-delete flagged messages + onFieldChange('autoDelete', v)} + onChange={(v) => onFieldChange('autoDelete', v)} disabled={saving} - aria-label="Toggle auto-delete" + label="Auto Delete" />
DM Notifications {(['warn', 'timeout', 'kick', 'ban'] as const).map((action) => (
- - {action} + onDmNotificationChange(action, v)} + onChange={(v) => onDmNotificationChange(action, v)} disabled={saving} - aria-label={`DM on ${action}`} + label={`DM on ${action}`} />
))}
- - Escalation Enabled + onEscalationChange(v)} + onChange={(v) => onEscalationChange(v)} disabled={saving} - aria-label="Toggle escalation" + label="Escalation" />
+ {/* Rate Limiting sub-section */} +
+ Rate Limiting +
+ Enabled + onRateLimitChange('enabled', v)} + disabled={saving} + label="Rate Limiting" + /> +
+
+ + +
+
+ + + +
+
+ + {/* Link Filtering sub-section */} +
+ Link Filtering +
+ Enabled + onLinkFilterChange('enabled', v)} + disabled={saving} + label="Link Filtering" + /> +
+ +
+ {/* Protect Roles sub-section */}
Protect Roles from Moderation
- - Enabled + onProtectRolesChange('enabled', v)} + onChange={(v) => onProtectRolesChange('enabled', v)} disabled={saving} - aria-label="Toggle protect roles" + label="Protect Roles" />
- - Include admins + onProtectRolesChange('includeAdmins', v)} + onChange={(v) => onProtectRolesChange('includeAdmins', v)} disabled={saving} - aria-label="Include admins" + label="Include admins" />
- - Include moderators + onProtectRolesChange('includeModerators', v)} + onChange={(v) => onProtectRolesChange('includeModerators', v)} disabled={saving} - aria-label="Include moderators" + label="Include moderators" />
- - Include server owner + onProtectRolesChange('includeServerOwner', v)} + onChange={(v) => onProtectRolesChange('includeServerOwner', v)} disabled={saving} - aria-label="Include server owner" + label="Include server owner" />
- - {guildId ? ( - onProtectRolesChange('roleIds', selected)} - disabled={saving} - placeholder="Select additional protected roles..." - /> - ) : ( -

Select a server first

- )} + + Additional protected role IDs (comma-separated) + + { + const raw = e.target.value; + onProtectRoleIdsRawChange(raw); + onProtectRolesChange( + 'roleIds', + raw + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + ); + }} + onBlur={(e) => + onProtectRoleIdsRawChange( + e.target.value + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + .join(', '), + ) + } + disabled={saving} + placeholder="Role ID 1, Role ID 2" + />
{/* Warning System Settings */} diff --git a/web/src/components/dashboard/config-sections/PermissionsSection.tsx b/web/src/components/dashboard/config-sections/PermissionsSection.tsx new file mode 100644 index 00000000..617b876a --- /dev/null +++ b/web/src/components/dashboard/config-sections/PermissionsSection.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { RoleSelector } from '@/components/ui/role-selector'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface PermissionsSectionProps { + draftConfig: GuildConfig; + guildId: string; + saving: boolean; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * Permissions configuration section. + * + * Provides controls for role-based access and bot owner overrides. + */ +export function PermissionsSection({ + draftConfig, + guildId, + saving, + onFieldChange, +}: PermissionsSectionProps) { + // Local state for bot owners raw input (parsed on blur) + const botOwnersDisplay = (draftConfig.permissions?.botOwners ?? []).join(', '); + const [botOwnersRaw, setBotOwnersRaw] = useState(botOwnersDisplay); + useEffect(() => { + setBotOwnersRaw(botOwnersDisplay); + }, [botOwnersDisplay]); + + return ( + + +
+
+ Permissions + Configure role-based access and bot owner overrides. +
+ onFieldChange('enabled', v)} + disabled={saving} + label="Permissions" + /> +
+
+ +
+ Admin Role + onFieldChange('adminRoleId', selected[0] ?? null)} + placeholder="Select admin role" + disabled={saving} + maxSelections={1} + /> +
+
+ Moderator Role + onFieldChange('moderatorRoleId', selected[0] ?? null)} + placeholder="Select moderator role" + disabled={saving} + maxSelections={1} + /> +
+ +
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/ReputationSection.tsx b/web/src/components/dashboard/config-sections/ReputationSection.tsx new file mode 100644 index 00000000..ca7bc44a --- /dev/null +++ b/web/src/components/dashboard/config-sections/ReputationSection.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Card, CardContent, CardTitle } from '@/components/ui/card'; +import { parseNumberInput } from '@/lib/config-normalization'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface ReputationSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onEnabledChange: (enabled: boolean) => void; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +const DEFAULT_LEVEL_THRESHOLDS = [100, 300, 600, 1000, 1500, 2500, 4000, 6000, 8500, 12000]; + +/** + * Reputation / XP configuration section. + * + * Provides controls for XP settings, cooldowns, level thresholds, and announcements. + */ +export function ReputationSection({ + draftConfig, + saving, + onEnabledChange, + onFieldChange, +}: ReputationSectionProps) { + const xpRange = draftConfig.reputation?.xpPerMessage ?? [5, 15]; + const levelThresholds = draftConfig.reputation?.levelThresholds ?? DEFAULT_LEVEL_THRESHOLDS; + + // Local state for level thresholds raw input (parsed on blur) + const thresholdsDisplay = levelThresholds.join(', '); + const [thresholdsRaw, setThresholdsRaw] = useState(thresholdsDisplay); + useEffect(() => { + setThresholdsRaw(thresholdsDisplay); + }, [thresholdsDisplay]); + + return ( + + +
+ Reputation / XP + +
+
+ + + + +
+ +
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/StarboardSection.tsx b/web/src/components/dashboard/config-sections/StarboardSection.tsx new file mode 100644 index 00000000..a20298bb --- /dev/null +++ b/web/src/components/dashboard/config-sections/StarboardSection.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { parseNumberInput } from '@/lib/config-normalization'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface StarboardSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * Starboard configuration section. + * + * Provides controls for pinning popular messages to a starboard channel, + * including threshold, emoji settings, and ignored channels. + */ +export function StarboardSection({ draftConfig, saving, onFieldChange }: StarboardSectionProps) { + return ( + + +
+
+ Starboard + Pin popular messages to a starboard channel. +
+ onFieldChange('enabled', v)} + disabled={saving} + label="Starboard" + /> +
+
+ + +
+ + +
+
+ Allow Self-Star + onFieldChange('selfStarAllowed', v)} + disabled={saving} + label="Self-Star Allowed" + /> +
+ +
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/TicketsSection.tsx b/web/src/components/dashboard/config-sections/TicketsSection.tsx new file mode 100644 index 00000000..202c02a7 --- /dev/null +++ b/web/src/components/dashboard/config-sections/TicketsSection.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { parseNumberInput } from '@/lib/config-normalization'; +import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; + +interface TicketsSectionProps { + draftConfig: GuildConfig; + saving: boolean; + onEnabledChange: (enabled: boolean) => void; + onFieldChange: (field: string, value: unknown) => void; +} + +/** Shared input styling for text inputs and selects. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * Tickets configuration section. + * + * Provides controls for ticket system mode, support roles, auto-close settings, + * and transcript channel configuration. + */ +export function TicketsSection({ + draftConfig, + saving, + onEnabledChange, + onFieldChange, +}: TicketsSectionProps) { + return ( + + +
+ Tickets + +
+
+ + +
+ + + + + +
+
+
+ ); +} diff --git a/web/src/components/dashboard/config-sections/TriageSection.tsx b/web/src/components/dashboard/config-sections/TriageSection.tsx index 28f45850..b19e6649 100644 --- a/web/src/components/dashboard/config-sections/TriageSection.tsx +++ b/web/src/components/dashboard/config-sections/TriageSection.tsx @@ -1,13 +1,9 @@ 'use client'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { ChannelSelector } from '@/components/ui/channel-selector'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { useGuildSelection } from '@/hooks/use-guild-selection'; +import { parseNumberInput } from '@/lib/config-normalization'; import type { GuildConfig } from '@/lib/config-utils'; -import { NumberField } from './NumberField'; +import { ToggleSwitch } from '../toggle-switch'; interface TriageSectionProps { draftConfig: GuildConfig; @@ -16,16 +12,15 @@ interface TriageSectionProps { onFieldChange: (field: string, value: unknown) => void; } +/** Shared input styling for text inputs. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + /** - * Renders the Triage configuration UI for editing classifier, responder, budget, timing, toggles, and moderation log channel. - * - * Renders nothing if `draftConfig.triage` is not present. + * Triage configuration section. * - * @param draftConfig - Guild configuration draft containing the `triage` settings to display and edit. - * @param saving - When true, input controls are disabled to prevent changes during a save operation. - * @param onEnabledChange - Invoked with the new enabled state when the Triage master switch is toggled. - * @param onFieldChange - Invoked with `(field, value)` for individual field updates; used for all editable triage fields including `moderationLogChannel`. - * @returns The Triage configuration card element, or `null` when triage configuration is absent. + * Provides controls for message triage classifier, responder models, + * budgets, intervals, and various feature toggles. */ export function TriageSection({ draftConfig, @@ -33,17 +28,8 @@ export function TriageSection({ onEnabledChange, onFieldChange, }: TriageSectionProps) { - const guildId = useGuildSelection(); - if (!draftConfig.triage) return null; - const moderationLogChannel = draftConfig.triage?.moderationLogChannel ?? ''; - const selectedChannels = moderationLogChannel ? [moderationLogChannel] : []; - - const handleChannelChange = (channels: string[]) => { - onFieldChange('moderationLogChannel', channels[0] ?? ''); - }; - return ( @@ -54,34 +40,36 @@ export function TriageSection({ Configure message triage classifier, responder models, and channels.
- -
- - + Classify Model + onFieldChange('classifyModel', e.target.value)} disabled={saving} + className={inputClasses} placeholder="e.g. claude-haiku-4-5" /> -
-
- - +
@@ -181,13 +169,138 @@ export function TriageSection({ onChange={handleChannelChange} placeholder="Select moderation log channel..." disabled={saving} - maxSelections={1} - filter="text" + className={inputClasses} + /> + + +
+ + +
+
+ + +
+
+ Streaming + onFieldChange('streaming', v)} + disabled={saving} + label="Streaming" + /> +
+
+ Moderation Response + onFieldChange('moderationResponse', v)} + disabled={saving} + label="Moderation Response" + /> +
+
+ Debug Footer + onFieldChange('debugFooter', v)} + disabled={saving} + label="Debug Footer" + /> +
+
+ Status Reactions + onFieldChange('statusReactions', v)} + disabled={saving} + label="Status Reactions" + /> +
+
); diff --git a/web/src/components/dashboard/config-sections/WelcomeSection.tsx b/web/src/components/dashboard/config-sections/WelcomeSection.tsx index 349ae39a..b73df4a5 100644 --- a/web/src/components/dashboard/config-sections/WelcomeSection.tsx +++ b/web/src/components/dashboard/config-sections/WelcomeSection.tsx @@ -1,24 +1,61 @@ 'use client'; +import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { Textarea } from '@/components/ui/textarea'; +import { RoleSelector } from '@/components/ui/role-selector'; import type { GuildConfig } from '@/lib/config-utils'; +import { ToggleSwitch } from '../toggle-switch'; interface WelcomeSectionProps { draftConfig: GuildConfig; + guildId: string; saving: boolean; + dmStepsRaw: string; onEnabledChange: (enabled: boolean) => void; onMessageChange: (message: string) => void; + onFieldChange: (field: string, value: unknown) => void; + onRoleMenuChange: (field: string, value: unknown) => void; + onDmSequenceChange: (field: string, value: unknown) => void; + onDmStepsRawChange: (value: string) => void; } +/** Shared input styling for text inputs and textareas. */ +const inputClasses = + 'w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'; + +/** + * Generate a UUID with fallback for environments without crypto.randomUUID. + */ +function generateId(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +/** + * Welcome Messages configuration section. + * + * Provides controls for welcome messages, role menu, and DM sequence settings. + */ export function WelcomeSection({ draftConfig, + guildId, saving, + dmStepsRaw, onEnabledChange, onMessageChange, + onFieldChange, + onRoleMenuChange, + onDmSequenceChange, + onDmStepsRawChange, }: WelcomeSectionProps) { + const roleMenuOptions = draftConfig.welcome?.roleMenu?.options ?? []; + return ( @@ -27,30 +64,179 @@ export function WelcomeSection({ Welcome Messages Greet new members when they join the server. - - -
- -