Message.utils.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { untrustedSql } from '@supabase/pg-meta'
  2. import { z, type SafeParseReturnType } from 'zod'
  3. // Splits markdown into alternating [plain, code, plain, code, ...] segments.
  4. // Odd-indexed segments are already inside code spans/fences and should be left alone.
  5. const CODE_SEGMENT_REGEX = /(```[\s\S]*?```|`[^`]*`)/g
  6. // Matches bare placeholder URLs like https://xxx/<project-ref>/... outside markdown link
  7. // syntax. Stops at whitespace, ), or ] to avoid consuming link delimiters. Trailing prose
  8. // punctuation is stripped in the replacement callback below.
  9. const PLACEHOLDER_URL_REGEX = /(?<!\()https?:\/\/[^\s)\]]*<[a-z][a-z0-9]*(?:-[a-z0-9]+)*>[^\s)\]]*/g
  10. /**
  11. * Wraps bare URLs containing <placeholder> patterns in backticks so they render in
  12. * code font, regardless of whether the LLM remembered to wrap them.
  13. */
  14. export function wrapPlaceholderUrls(markdown: string): string {
  15. if (!markdown.includes('<')) return markdown
  16. const segments = markdown.split(CODE_SEGMENT_REGEX)
  17. return segments
  18. .map((segment, i) => {
  19. if (i % 2 === 1) return segment
  20. return segment.replace(PLACEHOLDER_URL_REGEX, (url) => {
  21. const trailingPunct = url.match(/[.,;:!?'"]+$/)?.[0] ?? ''
  22. const cleanUrl = url.slice(0, url.length - trailingPunct.length)
  23. return `\`${cleanUrl}\`` + trailingPunct
  24. })
  25. })
  26. .join('')
  27. }
  28. // [Joshen] From https://github.com/remarkjs/react-markdown/blob/fda7fa560bec901a6103e195f9b1979dab543b17/lib/index.js#L425
  29. export function defaultUrlTransform(value: string) {
  30. const safeProtocol = /^(https?|ircs?|mailto|xmpp)$/i
  31. const colon = value.indexOf(':')
  32. const questionMark = value.indexOf('?')
  33. const numberSign = value.indexOf('#')
  34. const slash = value.indexOf('/')
  35. if (
  36. // If there is no protocol, it’s relative.
  37. colon === -1 ||
  38. // If the first colon is after a `?`, `#`, or `/`, it’s not a protocol.
  39. (slash !== -1 && colon > slash) ||
  40. (questionMark !== -1 && colon > questionMark) ||
  41. (numberSign !== -1 && colon > numberSign) ||
  42. // It is a protocol, it should be allowed.
  43. safeProtocol.test(value.slice(0, colon))
  44. ) {
  45. return value
  46. }
  47. return ''
  48. }
  49. const chartArgsSchema = z
  50. .object({
  51. view: z.enum(['table', 'chart']).optional(),
  52. xKey: z.string().optional(),
  53. xAxis: z.string().optional(),
  54. yKey: z.string().optional(),
  55. yAxis: z.string().optional(),
  56. })
  57. .passthrough()
  58. const chartArgsFieldSchema = z.preprocess((value) => {
  59. if (!value || typeof value !== 'object') return undefined
  60. if (Array.isArray(value)) return value[0]
  61. return value
  62. }, chartArgsSchema.optional())
  63. const executeSqlChartResultSchema = z
  64. .object({
  65. sql: z.string().optional(),
  66. label: z.string().optional(),
  67. isWriteQuery: z.boolean().optional(),
  68. chartConfig: chartArgsFieldSchema,
  69. config: chartArgsFieldSchema,
  70. })
  71. .passthrough()
  72. .transform(({ sql, label, isWriteQuery, chartConfig, config }) => {
  73. const chartArgs = chartConfig ?? config
  74. return {
  75. sql: untrustedSql(sql ?? ''),
  76. label,
  77. isWriteQuery,
  78. view: chartArgs?.view,
  79. xAxis: chartArgs?.xKey ?? chartArgs?.xAxis,
  80. yAxis: chartArgs?.yKey ?? chartArgs?.yAxis,
  81. }
  82. })
  83. export function parseExecuteSqlChartResult(
  84. input: unknown
  85. ): SafeParseReturnType<unknown, z.infer<typeof executeSqlChartResultSchema>> {
  86. return executeSqlChartResultSchema.safeParse(input)
  87. }
  88. export const deployEdgeFunctionInputSchema = z
  89. .object({
  90. code: z.string().min(1),
  91. name: z.string().trim().optional(),
  92. slug: z.string().trim().optional(),
  93. functionName: z.string().trim().optional(),
  94. label: z.string().optional(),
  95. })
  96. .passthrough()
  97. .transform((data) => {
  98. const rawName = data.functionName ?? data.name ?? data.slug
  99. const trimmedName = rawName?.trim()
  100. const functionName = trimmedName && trimmedName.length > 0 ? trimmedName : 'my-function'
  101. const rawLabel = data.label ?? rawName
  102. const trimmedLabel = rawLabel?.trim()
  103. const label = trimmedLabel && trimmedLabel.length > 0 ? trimmedLabel : 'Edge Function'
  104. return {
  105. code: data.code,
  106. functionName,
  107. label,
  108. }
  109. })
  110. export const deployEdgeFunctionOutputSchema = z
  111. .object({ success: z.boolean().optional() })
  112. .passthrough()
  113. export const rateMessageResponseSchema = z.object({
  114. category: z.enum([
  115. 'sql_generation',
  116. 'schema_design',
  117. 'rls_policies',
  118. 'edge_functions',
  119. 'database_optimization',
  120. 'debugging',
  121. 'general_help',
  122. 'other',
  123. ]),
  124. })
  125. export type RateMessageResponse = z.infer<typeof rateMessageResponseSchema>