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.
Package info
github.com/joelseneque/filament-help-requests
pkg:composer/joelseneque/filament-help-requests
Requires
- php: ^8.2
- filament/filament: ^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- livewire/livewire: ^3.0|^4.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.29
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.0
- v1.2.1
- v1.2.0
- v1.1.0
- v1.0.0
- dev-feat/issue-labels-hook
- dev-fix/help-request-label-name
- dev-feat/default-help-request-label
- dev-feat/parley
- dev-fix/settings-cluster-navigation
- dev-release/v1.2.0
- dev-feat/attachments
- dev-release/v1.1.0
- dev-feat/screenshot-markers
- dev-docs/claude-md
This package is auto-updated.
Last update: 2026-11-02 08:36:50 UTC
README
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, aname(orfirst_name/last_name) and theNotifiabletrait - A
notificationstable (php artisan make:notifications-tableif 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. ...
titledefaults to the first#heading; phrase it as the question, because articles about the current page are offered as starting questions.pagesare URL path patterns (*wildcards, no leading slash) the article is about.canhides 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.
Labels per issue
New GitHub issues get the Default labels from the settings page. To
choose labels per request, register a hook in a service provider's boot()
method, so queue workers have it too:
use Joelseneque\HelpRequests\HelpRequests; use Joelseneque\HelpRequests\Models\HelpRequest; HelpRequests::labelsUsing(fn (HelpRequest $request, array $defaults): array => [ ...$defaults, ...app(MyTriage::class)->labelsFor($request), ]);
The closure gets the request and the default labels and returns the full list. The labels are sent in the request that creates the issue, so anything that reacts to a new issue sees them straight away.
- The closure runs when the issue is created: usually in the queued
CreateGitHubIssueForHelpRequestjob, but inside the web request for the Create GitHub issue button and whenQUEUE_CONNECTION=sync. A slow closure makes that request wait. - Labels are trimmed, and blanks and repeats are dropped. Return
[]to create the issue without labels. - If the closure throws, the issue is still created with the default labels and a warning is logged.
- Create every label the closure can return in the repository first. If GitHub rejects the issue (422) and the closure changed the labels, the issue is created again with only the default labels and a warning is logged, so a missing label costs the labels, not the issue.
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. To choose labels per request, see
Labels per issue.
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.
-
Update the package.
composer update joelseneque/filament-help-requests
For a new major version, change the constraint in
composer.jsonfirst (e.g."^2.0"). -
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 migrateCommit the newly published migration files with the upgrade.
-
Check your published config, if you have one. Compare it with the package's
config/help-requests.phpand 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. -
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'sresources/views. -
Rebuild the panel theme, so Tailwind generates classes used by new widget markup:
npm run build
-
Clear caches:
php artisan optimize:clear
Restart queue workers (
php artisan queue:restart) if you use the GitHub integration. -
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.2.0
- New
HelpRequests::labelsUsing()hook to choose GitHub issue labels per request (see Labels per issue). Nothing changes until you register it. No migrations, config or view changes.
2.1.1
- The default issue label is now
help request(washelp-request). Only new settings rows get it — a site already on 2.1.0 keepshelp-requestuntil you change it on the settings page. No migrations, config or view changes.
2.1.0
- New installs start with the
help-requestdefault 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, andis_internal+parley_conversation_idonhelp_requests. - New config section
parley(step 3). SetPARLEY_ANTHROPIC_KEY(orANTHROPIC_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.mdto 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 everyscreenshot_path/screenshot_pathsvalue into a newattachmentslist 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'sstoragesection 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:
HasScreenshotsis nowHasAttachments.screenshotPaths(),screenshotUrls(),hasScreenshot()andgetScreenshotUrl()are replaced byattachmentPaths(),imageAttachmentPaths(),fileAttachmentPaths(),attachmentUrl($path),attachmentName($path)andhasAttachments(). The reply factory statewithScreenshot()is nowwithAttachment(). - Admin reply and status forms: the upload field is
attachments(wasscreenshot_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(default5) — add it to a published config'sstoragesection 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.