New

Just launched: Vueblocks, a brand-new search engine for Vue packages! Explore Vueblocks →

joelseneque/filament-help-requests

A Filament v5 help desk: a floating in-app help widget, an admin triage resource, email + database notifications, and optional two-way GitHub issue sync via a GitHub App.
171
Install
composer require joelseneque/filament-help-requests
Latest Version:v2.1.1
PHP:^8.2
Laravel:11 · 12 · 13
License:MIT
Last Updated:Oct 30, 2026
Links: GitHub  ·  Packagist
Maintainer: paripartners

Help Requests for Filament

A drop-in help desk for Filament v5 panels.

  • Parley, the help chat — the widget's first tab. People ask how to do something and get an answer from your app's own help library. When it can't answer, it says so and files the question for you to write up; when something sounds broken, it offers to turn the chat into a help request. See Parley.
  • Floating help button on every panel page. Users pick whether it is a bug or a feature request, describe it, and can attach up to 5 screenshots or files (PDF, Word, Excel, CSV, text) and a Loom video. Files keep their original name (made URL-safe). Screenshots can be pasted or dropped in, then marked with numbered pins ("mark where it goes wrong"); the pins are drawn onto the image and each pin's note is added to the request text. The page they were on is captured automatically.
  • "My requests" tab in the same widget: a collapsible list of their requests (one thread open at a time) where they read replies and reply back.
  • Admin triage — a resource to list, filter, reply (with attachments), change status and delete requests.
  • Notifications — email + in-app to admins on new requests and replies; email + in-app to the requester on replies and resolution.
  • Optional GitHub sync via a GitHub App — raise requests as issues, mirror replies as issue comments, and let closing or reopening the issue update the request.

Requirements

  • PHP 8.2+, Laravel 11–13, Filament 5, Livewire 3 or 4
  • A custom Filament theme (for the widget's styles — see Styles)
  • A user model with email, a name (or first_name / last_name) and the Notifiable trait
  • A notifications table (php artisan make:notifications-table if you don't have one)
  • A queue worker, if you use the GitHub integration

Installation

composer require joelseneque/filament-help-requests

Publish and run the migrations, and publish the config:

php artisan vendor:publish --tag="help-requests-migrations"
php artisan migrate
php artisan vendor:publish --tag="help-requests-config"

Make sure attachments can be served:

php artisan storage:link

Register the plugin

use Joelseneque\HelpRequests\HelpRequestsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->databaseNotifications() // requesters see replies in-app
        ->plugin(
            HelpRequestsPlugin::make()
                ->navigationGroup('Support'),
        );
}

->databaseNotifications() is needed for the in-app notifications. Without it, replies still arrive by email but users have no bell to see them in.

Who can manage requests

Out of the box, a user can see the admin list and the settings page when either $user->isAdmin() returns true or they have the super_admin role (spatie/laravel-permission). If your app has neither, nobody will see the admin pages until you say who can:

HelpRequestsPlugin::make()
    ->authorizeUsing(fn (User $user): bool => $user->is_staff)

Everyone who can sign in to the panel can raise requests.

Styles

The widget uses Tailwind utilities that Filament's own stylesheet does not include. Import the plugin stylesheet in your panel's theme, after the Filament import:

@import '../../../../vendor/filament/filament/resources/css/theme.css';
@import '../../../../vendor/joelseneque/filament-help-requests/resources/css/plugin.css';

Then rebuild your assets (npm run build). Without this the help button renders unstyled.

The widget

Page name

The widget shows About {page} above the form, taken from the browser tab title with the panel's brand name removed ("Deals — record - Acme" becomes "Deals — record"). It follows wire:navigate page changes and is saved as the request's page, alongside the full URL.

Feedback categories

Users can tag a request with one of these buttons (optional):

Key Label
bug Bug
feature Feature

Requests filed under the five categories offered before 2.0 (change, missing, broken, confusing, works_well) keep their old labels.

Replace them with your own list:

HelpRequestsPlugin::make()
    ->categories([
        'idea' => 'I have an idea',
        'broken' => 'Looks broken',
        'Needs a fix', // no key: stored as 'needs_a_fix'
    ])

Pass ->categories([]) to hide the buttons. A closure is also accepted if the list depends on something at runtime.

Categories are stored by key, so relabelling one keeps old requests correct. A key you later remove still shows a readable label on old requests. The category appears in the admin list (with a filter), on the request page, in the admin email and in the GitHub issue title.

Loom videos

Users can paste a Loom share link with their request. Admins get the video embedded on the request page, with an Open in Loom link.

The Record a Loom link beside the field first tries to open the Loom desktop app (loomDesktop://). If the page hasn't lost focus about a second later, it assumes the app isn't installed and opens loom.com in a new tab. Cmd/Ctrl-click always goes straight to the website. Because browsers can't report whether an app is installed, this is a best guess:

  • Chrome asks "Open Loom?" the first time; loom.com may also open behind that prompt until the user ticks "Always allow".
  • Safari may show an "address is invalid" alert before falling back when the app isn't installed.

Turn the field off per panel:

HelpRequestsPlugin::make()->videoLinks(false)

Other options live in config/help-requests.php under video:

'video' => [
    'enabled' => true,                          // fallback when ->videoLinks() isn't called
    'record_url' => 'https://www.loom.com/',    // where "Record a Loom" goes
    'record_app_url' => 'loomDesktop://',       // null = always go to record_url
    'allowed_hosts' => ['loom.com'],            // [] = accept any https link
],

Links must be https and on an allowed host (subdomains included). Links from other hosts are shown as a plain link rather than an embedded player.

Parley, the help chat

Parley is the widget's Ask Parley tab. It answers questions about your app from a help library you write, and it is shown only when an Anthropic API key is configured (PARLEY_ANTHROPIC_KEY, falling back to ANTHROPIC_API_KEY).

The help library

The library is markdown in your app's repository, resources/help by default, so it is written next to the code it describes and deployed with it.

resources/help/
├── _about.md                 # the primer: read on every question
├── _glossary.md              # also primer (any file starting with _)
├── quotes/
│   └── sending-a-quote.md    # an article, slug "quotes/sending-a-quote"
└── approving-timesheets.md
  • The primer (_*.md) says what the app is, who uses it, and the words your business uses. Write it first: with a primer and no articles Parley can already tell people where things are, from the panel's navigation.

  • An article answers one question. Optional front matter:

    ---
    title: How do I send a quote?
    summary: Emailing a quote to the client from the quote's page.
    pages: ['*/quotes', '*/quotes/*']
    can: [send_quotes]
    ---
    
    1. Open the quote. ...
    

    title defaults to the first # heading; phrase it as the question, because articles about the current page are offered as starting questions. pages are URL path patterns (* wildcards, no leading slash) the article is about. can hides the article from anyone who lacks all of the listed abilities ($user->can()).

Up to parley.inline_kb (150 KB) of articles go into every prompt and are cached; anything past that is listed by title for Parley to open on demand. Run php artisan help-requests:parley to see the library as Parley reads it, and the questions waiting for an article.

What Parley knows

  • The primer and the articles the person may read.
  • The screens they can open, read from the panel's navigation, so it can say where something is and link to it.
  • Who is asking: their name and, with spatie/laravel-permission, their roles (->describeParleyUserUsing() replaces this).
  • The page they asked from.

It cannot see records. Give it lookups with ->parleyTools([...]): classes implementing Joelseneque\HelpRequests\Parley\Contracts\ParleyTool. A tool runs as the person asking, so query through your app's scoped queries and policies.

When it can't answer

Parley is told to say so rather than guess, and to file the question. The question becomes an internal help request of type Help article, linked to the chat: it is in the admin list (filter Raised by → Parley), goes to GitHub when issues are raised automatically, and never appears in the asker's own list or emails them. More gaps in the same chat are added to it as replies. A thumbs down on an answer files one too. Write the article, then resolve the request.

Chats

Each day starts a new chat; reopening the widget the same day carries on today's. Chats lists earlier ones grouped by day (Today, Yesterday, then the date), and any of them can be opened and continued. New chat starts a fresh one at any time. Once a chat passes parley.condense_after messages (6), the earlier ones fold into a list of their questions, with Show all to open them.

The day runs on ->parleyTimezone() (a string or a closure, e.g. the current tenant's timezone), then parley.timezone, then the app's timezone.

Turning a chat into a request

When someone describes a bug or wants a feature, Parley offers a Raise bug request button that opens the request form filled in. "Still stuck? Raise a help request" does the same from any chat. The request carries the chat, shown on its admin page.

Options

HelpRequestsPlugin::make()
    ->parley()                                   // false hides the tab
    ->parleyName('Parley')
    ->parleyKnowledgePaths([resource_path('help')])
    ->parleyTools([MyOpenJobs::class])
    ->parleyTimezone('Australia/Melbourne')
    ->describeParleyUserUsing(fn (User $user): string => "{$user->name}, {$user->position}")

The model, effort, limits and key are under parley in the config. The default model is claude-opus-5-5 at low effort. A question the model's safety checks decline is re-run on Anthropic's recommended fallback model (parley.fallbacks).

Linking to the widget

  • Open a user's threads from your own code: dispatch the Livewire event open-help-requests.
  • Link straight to a thread: any panel URL with ?help_request={id}. This is how the requester's notifications link back, because the admin page would deny them.

Configuration

config/help-requests.php:

Key Default Purpose
user_model App\Models\User The model that raises and answers requests.
user_searchable_columns ['name', 'email'] Columns searched by the admin list's "From" column.
recipient HELP_REQUEST_EMAIL Inbox emailed about new requests and replies. null = no email.
admin_role super_admin Role used by the default manage check and for in-app admin alerts.
notifications.channels ['database', 'mail'] Channels used for requester notifications.
video.* see above Loom link options.
storage.* public, help-requests, 5120 KB, 5 files Attachment disk, directory, max size per file and max attachments per request or reply. The disk must serve public URLs — GitHub issues and emails link the files directly.
storage.accepted_file_types images, PDF, Word, Excel, CSV, text MIME types users and admins may attach. Uploads must match by content and by file extension. Don't add HTML, SVG or scripts: served from your domain they would run as your site.
branding.name / branding.logo_url APP_NAME / none Shown in emails and GitHub issue text.
routes.* webhooks/github, github/connect, github/callback Route paths and install-route middleware.
github.* GITHUB_APP_* env vars GitHub App credentials; schedule_sync toggles the hourly poll.

Admins who get the in-app alert: the user whose email matches recipient, otherwise everyone with admin_role.

Plugin options

All options apply app-wide, including queued jobs and the webhook.

HelpRequestsPlugin::make()
    // Navigation
    ->navigationGroup('Support')
    ->navigationSort(90)
    ->settingsCluster(SettingsCluster::class)   // put the settings page in a cluster
    ->settingsNavigationGroup('Settings')
    ->settingsNavigationSort(10)

    // Widget
    ->categories([...])                          // or [] to hide
    ->videoLinks(false)                          // hide the Loom field

    // Access
    ->authorizeUsing(fn (User $user) => $user->hasRole('support'))
    ->authorizeSettingsUsing(fn (User $user) => $user->hasRole('super_admin'))

    // Who gets the in-app "new request / new reply" alert
    ->adminRecipientsUsing(fn () => User::role('support')->get())

    // Take over requester notifications, e.g. to honour user preferences.
    // $type is HelpRequestNotifier::TYPE_REPLY or TYPE_RESOLVED.
    ->sendNotificationsUsing(function (User $user, Notification $notification, string $type, array $context) {
        $user->notify($notification->onlyVia(['database']));

        if ($user->wantsEmailFor($type)) {
            $user->notify($notification->onlyVia(['mail']));
        }
    })

    // Turn parts off in this panel
    ->widget(false)
    ->resource(false)
    ->settingsPage(false);

With several panels, register the plugin on each panel where it should appear. ->resource(false) / ->widget(false) let you put the admin pages in one panel and the help button in another.

GitHub integration (optional)

Requests can be raised as issues in a GitHub repository, either automatically or with a button on each request. Replies written in the app are posted to the issue. Closing an issue marks the request Resolved, closing it as not planned marks it Closed, and reopening it marks it Open. Comments made on the issue stay in GitHub — they are not shown to the requester.

Create a GitHub App for each site

A GitHub App has one Setup URL and one Webhook URL, so create a separate app for each site that uses this package. Sharing one app between sites breaks the connect flow and sends every webhook to the first site.

In GitHub: Settings → Developer settings → GitHub Apps → New GitHub App

  • Setup URL: https://your-app.test/github/callback
  • Webhook URL: https://your-app.test/webhooks/github
  • Webhook secret: any random string
  • Repository permissions → Issues: Read and write
  • Subscribe to events: Issues, Installation

Generate a private key, then add to .env:

GITHUB_APP_ID=
GITHUB_APP_SLUG=              # from the app's public URL: github.com/apps/{slug}
GITHUB_APP_PRIVATE_KEY=       # raw PEM, base64 of the PEM, or a path to the .pem file
GITHUB_APP_WEBHOOK_SECRET=

A path to the .pem must exist on every server the app runs on. Pasting the base64 of the .pem avoids that.

Connect

Open Help Request Settings in the panel, click Connect to GitHub, choose which repositories the app can access, then pick the repository for this site and save. The connection is stored in this site's database, so each environment (local, staging, production) connects separately.

New issues get the labels under Default labels, which starts as help request so issues raised from the app are easy to filter. Add more (for example AI) or clear it on the same page. Create the labels in the repository first.

Queue and schedule

Automatic issue creation runs on the queue, so a worker must be running. An hourly help-requests:sync-github is scheduled automatically to catch any missed webhooks; your scheduler (php artisan schedule:run) must be running. Turn it off with github.schedule_sync => false.

php artisan help-requests:github-status   # checks env, settings, queue and a live connection
php artisan help-requests:sync-github     # poll linked issues now; --all includes finished ones

Upgrading

Follow these steps in each app that uses the package.

  1. Update the package.

    composer update joelseneque/filament-help-requests
    

    For a new major version, change the constraint in composer.json first (e.g. "^2.0").

  2. Publish and run new migrations. Files you already published are skipped, so only new ones are copied:

    php artisan vendor:publish --tag="help-requests-migrations"
    php artisan migrate
    

    Commit the newly published migration files with the upgrade.

  3. Check your published config, if you have one. Compare it with the package's config/help-requests.php and copy over new keys you want to change. Laravel only merges top-level sections, so a new key inside a section you published (e.g. storage.max_files) is missing from your file and uses its built-in default until you add it.

  4. Check overridden views, if you published any to resources/views/vendor/help-requests. They will not pick up the package's changes; diff them against the package's resources/views.

  5. Rebuild the panel theme, so Tailwind generates classes used by new widget markup:

    npm run build
    
  6. Clear caches:

    php artisan optimize:clear
    

    Restart queue workers (php artisan queue:restart) if you use the GitHub integration.

  7. Smoke test: open a panel page, send a help request from the ? button, reply to it from the admin resource and from My requests.

Version notes

See CHANGELOG.md for the full list of changes.

2.1.1

  • The default issue label is now help request (was help-request). Only new settings rows get it — a site already on 2.1.0 keeps help-request until you change it on the settings page. No migrations, config or view changes.

2.1.0

  • New installs start with the help-request default issue label. Existing settings are unchanged — add the label on the settings page if you want it. No migrations, config or view changes.

2.0.0

  • New migration create_parley_tables (step 2): the chat's conversations and messages, and is_internal + parley_conversation_id on help_requests.
  • New config section parley (step 3). Set PARLEY_ANTHROPIC_KEY (or ANTHROPIC_API_KEY) to show the chat; without one the widget is as before.
  • The default categories are now Bug and Feature. Old requests keep their labels. Pass ->categories([...]) to keep the old five.
  • The admin resource declares $isScopedToTenant = false, so it lists every request on a multi-tenant panel instead of failing to scope.
  • New widget markup and the view components/parley-transcript: rebuild the theme (step 5) and re-diff overridden views (step 4).
  • Add a resources/help/_about.md to your app — see Parley.

1.2.1

  • Fix: ->settingsCluster() now lists the settings page in that cluster's navigation. Before, the page was routed under the cluster but missing from its menu. No migrations, config or view changes.

1.2.0

  • New migration add_attachments_to_help_requests_tables (step 2). It moves every screenshot_path / screenshot_paths value into a new attachments list and drops both screenshot columns — update any of your own code that reads them.
  • New config key storage.accepted_file_types — add it to a published config's storage section to change what can be attached (step 3).
  • New widget markup and email partial emails/partials/attachments: rebuild the theme (step 5) and re-diff overridden views (step 4).
  • Models: HasScreenshots is now HasAttachments. screenshotPaths(), screenshotUrls(), hasScreenshot() and getScreenshotUrl() are replaced by attachmentPaths(), imageAttachmentPaths(), fileAttachmentPaths(), attachmentUrl($path), attachmentName($path) and hasAttachments(). The reply factory state withScreenshot() is now withAttachment().
  • Admin reply and status forms: the upload field is attachments (was screenshot_paths). Only matters if you tested those actions.

1.1.0

  • New migration add_screenshot_paths_to_help_requests_tables (step 2).
  • New config key storage.max_files (default 5) — add it to a published config's storage section to change it (step 3).
  • New widget markup: rebuild the theme (step 5).
  • Livewire widget properties renamed: screenshot → screenshots, replyScreenshot → replyScreenshots. Only matters if you extended or tested the widget.

Trying an unreleased version in your app

To test a local checkout of this package in an app before it is released, point the app at the checkout with a Composer path repository:

"repositories": [
    { "type": "path", "url": "../filament-help-requests", "options": { "symlink": true } }
]
composer require "joelseneque/filament-help-requests:@dev"

Then follow steps 2–7. When you are done, remove the repository entry and run composer require "joelseneque/filament-help-requests:^1.0" again, so the app's composer.json and composer.lock don't point at your local copy.

Testing

composer install
composer test

License

MIT. See LICENSE.

Related Packages

jeffersongoncalves/filament-help-desk

Filament plugins for jeffersongoncalves/laravel-help-desk — User, Operator, and...

3,591 12
log1x/filament-starter

A beautiful starting point for your next TALL stack application.

2,088 527
packstub/filament-session-replay

Session replay inside a Filament panel: every recording of packstub/session-repl...

375 8
mmes-design/filament-file-manager

A beautiful file manager plugin for Filament. Browse, upload, rename, move and d...

5,182 16
rdp77/client-management

A beautiful your Client Management with TALL stack application.

3 0

Version History

Version Released PHP Laravel License
v2.1.1 ^8.2 ^11.0|^12.0|^13.0 MIT
v2.1.0 ^8.2 ^11.0|^12.0|^13.0 MIT
v2.0.0 ^8.2 ^11.0|^12.0|^13.0 MIT
v1.2.1 ^8.2 ^11.0|^12.0|^13.0 MIT
v1.2.0 ^8.2 ^11.0|^12.0|^13.0 MIT