Files
supabase/apps/studio/components/interfaces/Support/SupportAssistant.utils.ts
Saxon Fletcher 033daf223c Support form Assistant Streamdown (#46248)
Re-adds support form Assistant response using a lighter weight
Streamdown component vs the more heavy `Message` component.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* AI Assistant follow-up card after ticket submission for project-scoped
requests.
* In-chat support request preview panels showing submitted subject and
message.

* **Improvements**
* Smarter project selection when opening the support form via
route/context.
* Success screen: cleaner layout, project-name messaging, optional
finish action, and a "Join Discord" button.
  * Category prompt text updated to "What issue are you having?"
  * New success/feedback section for consistent layouts.

* **Tests**
* Added tests covering support prompt serialization/parsing and UI
previews.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46248?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-26 09:56:52 +00:00

78 lines
2.7 KiB
TypeScript

import type { SubmittedSupportRequest } from './SupportForm.state'
const SUPPORT_ASSISTANT_FIELD_LABELS = {
assistant_context: 'Assistant context',
organization_slug: 'Organization',
project_ref: 'Project',
category: 'Category',
severity: 'Severity',
subject: 'Subject',
message: 'Message',
affected_services: 'Affected services',
library: 'Client library',
support_access: 'Support access',
dashboard_logs: 'Dashboard logs',
} as const
export type ParsedSupportAssistantPrompt = Partial<
Record<keyof typeof SUPPORT_ASSISTANT_FIELD_LABELS, string>
>
function escapeSupportTagValue(value: string | boolean | undefined) {
if (value === undefined || value === '') return 'Not provided'
return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function unescapeSupportTagValue(value: string) {
return value.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim()
}
function supportField(
name: keyof typeof SUPPORT_ASSISTANT_FIELD_LABELS,
value: string | boolean | undefined
) {
return ` <${name}>${escapeSupportTagValue(value)}</${name}>`
}
export function buildSupportAssistantPrompt(request: SubmittedSupportRequest) {
return [
'<support>',
supportField(
'assistant_context',
'A support request has already been submitted and a human member of the Supabase Support team is already looking at it. Your role is to help the user troubleshoot in the interim while they wait for the human support response. Do not ask them to submit another support ticket for this same issue.'
),
supportField('category', request.category),
supportField('severity', request.severity),
supportField('subject', request.subject),
supportField('message', request.message),
supportField('affected_services', request.affectedServices),
supportField('library', request.library),
supportField('support_access', request.allowSupportAccess ? 'Granted' : 'Not granted'),
supportField('dashboard_logs', request.dashboardLogs ? 'Attached' : 'Not attached'),
'</support>',
].join('\n')
}
export function parseSupportAssistantPrompt(text: string): ParsedSupportAssistantPrompt | null {
const supportMatch = text.match(/<support>([\s\S]*?)<\/support>/i)
if (!supportMatch) return null
const parsed = Object.keys(SUPPORT_ASSISTANT_FIELD_LABELS).reduce<ParsedSupportAssistantPrompt>(
(acc, field) => {
const fieldMatch = supportMatch[1].match(
new RegExp(`<${field}>([\\s\\S]*?)<\\/${field}>`, 'i')
)
if (fieldMatch) {
acc[field as keyof typeof SUPPORT_ASSISTANT_FIELD_LABELS] = unescapeSupportTagValue(
fieldMatch[1]
)
}
return acc
},
{}
)
return Object.keys(parsed).length > 0 ? parsed : null
}