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