@seed-ship/mcp-ui-solid 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +50 -1
  2. package/dist/components/AutocompleteDropdown.cjs +201 -0
  3. package/dist/components/AutocompleteDropdown.cjs.map +1 -0
  4. package/dist/components/AutocompleteDropdown.d.ts +71 -0
  5. package/dist/components/AutocompleteDropdown.d.ts.map +1 -0
  6. package/dist/components/AutocompleteDropdown.js +201 -0
  7. package/dist/components/AutocompleteDropdown.js.map +1 -0
  8. package/dist/components/AutocompleteFormField.cjs +289 -0
  9. package/dist/components/AutocompleteFormField.cjs.map +1 -0
  10. package/dist/components/AutocompleteFormField.d.ts +52 -0
  11. package/dist/components/AutocompleteFormField.d.ts.map +1 -0
  12. package/dist/components/AutocompleteFormField.js +289 -0
  13. package/dist/components/AutocompleteFormField.js.map +1 -0
  14. package/dist/components/DraggableGridItem.cjs +133 -0
  15. package/dist/components/DraggableGridItem.cjs.map +1 -0
  16. package/dist/components/DraggableGridItem.d.ts +95 -0
  17. package/dist/components/DraggableGridItem.d.ts.map +1 -0
  18. package/dist/components/DraggableGridItem.js +133 -0
  19. package/dist/components/DraggableGridItem.js.map +1 -0
  20. package/dist/components/EditableUIResourceRenderer.cjs +203 -0
  21. package/dist/components/EditableUIResourceRenderer.cjs.map +1 -0
  22. package/dist/components/EditableUIResourceRenderer.d.ts +43 -0
  23. package/dist/components/EditableUIResourceRenderer.d.ts.map +1 -0
  24. package/dist/components/EditableUIResourceRenderer.js +203 -0
  25. package/dist/components/EditableUIResourceRenderer.js.map +1 -0
  26. package/dist/components/GhostText.cjs +105 -0
  27. package/dist/components/GhostText.cjs.map +1 -0
  28. package/dist/components/GhostText.d.ts +113 -0
  29. package/dist/components/GhostText.d.ts.map +1 -0
  30. package/dist/components/GhostText.js +105 -0
  31. package/dist/components/GhostText.js.map +1 -0
  32. package/dist/components/ResizeHandle.cjs +173 -0
  33. package/dist/components/ResizeHandle.cjs.map +1 -0
  34. package/dist/components/ResizeHandle.d.ts +50 -0
  35. package/dist/components/ResizeHandle.d.ts.map +1 -0
  36. package/dist/components/ResizeHandle.js +173 -0
  37. package/dist/components/ResizeHandle.js.map +1 -0
  38. package/dist/context/AutocompleteContext.cjs +158 -0
  39. package/dist/context/AutocompleteContext.cjs.map +1 -0
  40. package/dist/context/AutocompleteContext.d.ts +77 -0
  41. package/dist/context/AutocompleteContext.d.ts.map +1 -0
  42. package/dist/context/AutocompleteContext.js +158 -0
  43. package/dist/context/AutocompleteContext.js.map +1 -0
  44. package/dist/hooks/index.d.ts +6 -0
  45. package/dist/hooks/index.d.ts.map +1 -1
  46. package/dist/hooks/useAutocomplete.cjs +234 -0
  47. package/dist/hooks/useAutocomplete.cjs.map +1 -0
  48. package/dist/hooks/useAutocomplete.d.ts +119 -0
  49. package/dist/hooks/useAutocomplete.d.ts.map +1 -0
  50. package/dist/hooks/useAutocomplete.js +234 -0
  51. package/dist/hooks/useAutocomplete.js.map +1 -0
  52. package/dist/hooks/useDragDrop.cjs +170 -0
  53. package/dist/hooks/useDragDrop.cjs.map +1 -0
  54. package/dist/hooks/useDragDrop.d.ts +100 -0
  55. package/dist/hooks/useDragDrop.d.ts.map +1 -0
  56. package/dist/hooks/useDragDrop.js +170 -0
  57. package/dist/hooks/useDragDrop.js.map +1 -0
  58. package/dist/hooks/useResize.cjs +209 -0
  59. package/dist/hooks/useResize.cjs.map +1 -0
  60. package/dist/hooks/useResize.d.ts +87 -0
  61. package/dist/hooks/useResize.d.ts.map +1 -0
  62. package/dist/hooks/useResize.js +209 -0
  63. package/dist/hooks/useResize.js.map +1 -0
  64. package/dist/hooks.cjs +6 -0
  65. package/dist/hooks.cjs.map +1 -1
  66. package/dist/hooks.d.cts +6 -0
  67. package/dist/hooks.d.ts +6 -0
  68. package/dist/hooks.js +6 -0
  69. package/dist/hooks.js.map +1 -1
  70. package/dist/index.cjs +29 -0
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.d.cts +18 -3
  73. package/dist/index.d.ts +18 -3
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +29 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/plugins/duckdb.cjs +192 -0
  78. package/dist/plugins/duckdb.cjs.map +1 -0
  79. package/dist/plugins/duckdb.d.ts +20 -0
  80. package/dist/plugins/duckdb.d.ts.map +1 -0
  81. package/dist/plugins/duckdb.js +170 -0
  82. package/dist/plugins/duckdb.js.map +1 -0
  83. package/dist/plugins/groq.cjs +97 -0
  84. package/dist/plugins/groq.cjs.map +1 -0
  85. package/dist/plugins/groq.d.ts +13 -0
  86. package/dist/plugins/groq.d.ts.map +1 -0
  87. package/dist/plugins/groq.js +97 -0
  88. package/dist/plugins/groq.js.map +1 -0
  89. package/dist/plugins/index.d.ts +10 -0
  90. package/dist/plugins/index.d.ts.map +1 -0
  91. package/dist/plugins/rest.cjs +92 -0
  92. package/dist/plugins/rest.cjs.map +1 -0
  93. package/dist/plugins/rest.d.ts +13 -0
  94. package/dist/plugins/rest.d.ts.map +1 -0
  95. package/dist/plugins/rest.js +92 -0
  96. package/dist/plugins/rest.js.map +1 -0
  97. package/dist/plugins/supabase.cjs +79 -0
  98. package/dist/plugins/supabase.cjs.map +1 -0
  99. package/dist/plugins/supabase.d.ts +13 -0
  100. package/dist/plugins/supabase.d.ts.map +1 -0
  101. package/dist/plugins/supabase.js +79 -0
  102. package/dist/plugins/supabase.js.map +1 -0
  103. package/dist/services/validation.cjs +40 -1
  104. package/dist/services/validation.cjs.map +1 -1
  105. package/dist/services/validation.d.ts.map +1 -1
  106. package/dist/services/validation.js +40 -1
  107. package/dist/services/validation.js.map +1 -1
  108. package/dist/types/index.d.ts +430 -0
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types.d.cts +430 -0
  111. package/dist/types.d.ts +430 -0
  112. package/package.json +16 -1
  113. package/src/components/AutocompleteDropdown.tsx +329 -0
  114. package/src/components/AutocompleteFormField.tsx +288 -0
  115. package/src/components/DraggableGridItem.tsx +274 -0
  116. package/src/components/EditableUIResourceRenderer.tsx +273 -0
  117. package/src/components/GhostText.tsx +262 -0
  118. package/src/components/ResizeHandle.tsx +262 -0
  119. package/src/context/AutocompleteContext.tsx +317 -0
  120. package/src/hooks/index.ts +23 -0
  121. package/src/hooks/useAutocomplete.test.ts +334 -0
  122. package/src/hooks/useAutocomplete.ts +466 -0
  123. package/src/hooks/useDragDrop.test.ts +355 -0
  124. package/src/hooks/useDragDrop.ts +379 -0
  125. package/src/hooks/useResize.test.ts +313 -0
  126. package/src/hooks/useResize.ts +372 -0
  127. package/src/index.ts +71 -0
  128. package/src/plugins/duckdb.ts +269 -0
  129. package/src/plugins/groq.ts +137 -0
  130. package/src/plugins/index.ts +14 -0
  131. package/src/plugins/rest.ts +147 -0
  132. package/src/plugins/supabase.ts +120 -0
  133. package/src/services/validation.ts +46 -0
  134. package/src/styles/autocomplete.css +356 -0
  135. package/src/styles/drag-drop.css +297 -0
  136. package/src/styles/index.css +7 -0
  137. package/src/types/index.ts +529 -0
  138. package/src/vite-env.d.ts +18 -0
  139. package/tsconfig.tsbuildinfo +1 -1
  140. package/vite.config.ts +2 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * DuckDB WASM Autocomplete Plugin
3
+ * Provides SQL-based suggestions using DuckDB WASM
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ *
7
+ * Note: DuckDB WASM is ~2MB and is lazy-loaded on first use.
8
+ * This plugin requires @duckdb/duckdb-wasm as an optional peer dependency.
9
+ */
10
+
11
+ import type {
12
+ AutocompletePlugin,
13
+ AutocompleteResult,
14
+ AutocompleteContext,
15
+ AutocompleteOption,
16
+ DuckDBPluginConfig
17
+ } from '../types'
18
+
19
+ // Type for DuckDB connection (lazy loaded)
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ type DuckDBConnection = any
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ type DuckDBInstance = any
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ type DuckDBModule = any
26
+
27
+ // Module state for DuckDB (singleton)
28
+ let duckdbPromise: Promise<{ db: DuckDBInstance; conn: DuckDBConnection }> | null = null
29
+
30
+ /**
31
+ * Check if we're in a test environment
32
+ */
33
+ function isTestEnvironment(): boolean {
34
+ return typeof process !== 'undefined' && (
35
+ process.env.NODE_ENV === 'test' ||
36
+ process.env.VITEST === 'true' ||
37
+ typeof (globalThis as any).vitest !== 'undefined'
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Lazy load DuckDB WASM
43
+ */
44
+ async function loadDuckDB(): Promise<{ db: DuckDBInstance; conn: DuckDBConnection }> {
45
+ // In test environment, throw early to avoid import issues
46
+ if (isTestEnvironment()) {
47
+ throw new Error('[DuckDB Plugin] DuckDB WASM is not available in test environment')
48
+ }
49
+
50
+ if (duckdbPromise) {
51
+ return duckdbPromise
52
+ }
53
+
54
+ duckdbPromise = (async () => {
55
+ try {
56
+ // Dynamic import to avoid bundling if not used
57
+ // The import is wrapped to handle missing module gracefully
58
+ let duckdb: DuckDBModule
59
+ try {
60
+ duckdb = await import(/* @vite-ignore */ '@duckdb/duckdb-wasm')
61
+ } catch (importError) {
62
+ throw new Error(
63
+ '[DuckDB Plugin] @duckdb/duckdb-wasm is not installed. ' +
64
+ 'Install it with: npm install @duckdb/duckdb-wasm'
65
+ )
66
+ }
67
+
68
+ // Get WASM bundles
69
+ const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles()
70
+
71
+ // Select best bundle for the browser
72
+ const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES)
73
+
74
+ // Instantiate worker and database
75
+ const worker = new Worker(bundle.mainWorker!)
76
+ const logger = new duckdb.ConsoleLogger()
77
+ const db = new duckdb.AsyncDuckDB(logger, worker)
78
+
79
+ await db.instantiate(bundle.mainModule, bundle.pthreadWorker)
80
+
81
+ // Create connection
82
+ const conn = await db.connect()
83
+
84
+ return { db, conn }
85
+ } catch (error) {
86
+ console.error('[DuckDB Plugin] Failed to load DuckDB WASM:', error)
87
+ duckdbPromise = null
88
+ throw error
89
+ }
90
+ })()
91
+
92
+ return duckdbPromise
93
+ }
94
+
95
+ /**
96
+ * Create a DuckDB WASM autocomplete plugin
97
+ */
98
+ export function createDuckDBPlugin(config: DuckDBPluginConfig): AutocompletePlugin {
99
+ const { query, data } = config
100
+
101
+ let isReady = false
102
+ let initPromise: Promise<void> | null = null
103
+ let connection: DuckDBConnection | null = null
104
+
105
+ /**
106
+ * Initialize DuckDB and load data if provided
107
+ */
108
+ const initialize = async (): Promise<void> => {
109
+ if (initPromise) return initPromise
110
+
111
+ initPromise = (async () => {
112
+ try {
113
+ const { conn } = await loadDuckDB()
114
+ connection = conn
115
+
116
+ // Load data if provided
117
+ if (data) {
118
+ await loadData(conn, data)
119
+ }
120
+
121
+ isReady = true
122
+ } catch (error) {
123
+ console.error('[DuckDB Plugin] Initialization error:', error)
124
+ throw error
125
+ }
126
+ })()
127
+
128
+ return initPromise
129
+ }
130
+
131
+ /**
132
+ * Load data into DuckDB
133
+ */
134
+ const loadData = async (
135
+ conn: DuckDBConnection,
136
+ dataConfig: NonNullable<DuckDBPluginConfig['data']>
137
+ ): Promise<void> => {
138
+ const { tableName, source, format = 'csv' } = dataConfig
139
+
140
+ try {
141
+ if (typeof source === 'string') {
142
+ // URL - fetch and load
143
+ if (source.startsWith('http://') || source.startsWith('https://')) {
144
+ if (format === 'csv') {
145
+ await conn.query(`
146
+ CREATE TABLE IF NOT EXISTS ${tableName} AS
147
+ SELECT * FROM read_csv_auto('${source}')
148
+ `)
149
+ } else if (format === 'json') {
150
+ await conn.query(`
151
+ CREATE TABLE IF NOT EXISTS ${tableName} AS
152
+ SELECT * FROM read_json_auto('${source}')
153
+ `)
154
+ } else if (format === 'parquet') {
155
+ await conn.query(`
156
+ CREATE TABLE IF NOT EXISTS ${tableName} AS
157
+ SELECT * FROM read_parquet('${source}')
158
+ `)
159
+ }
160
+ } else {
161
+ // Inline data (CSV/JSON string)
162
+ // For inline data, we'd need to use DuckDB's data registration
163
+ console.warn('[DuckDB Plugin] Inline data not yet supported, use URL instead')
164
+ }
165
+ }
166
+ } catch (error) {
167
+ console.error('[DuckDB Plugin] Error loading data:', error)
168
+ throw error
169
+ }
170
+ }
171
+
172
+ return {
173
+ id: 'duckdb',
174
+ name: 'DuckDB WASM',
175
+
176
+ configure(newConfig: Record<string, any>) {
177
+ // Allow runtime reconfiguration
178
+ Object.assign(config, newConfig)
179
+ },
180
+
181
+ isReady() {
182
+ return isReady
183
+ },
184
+
185
+ async getSuggestions(
186
+ input: string,
187
+ _context?: AutocompleteContext
188
+ ): Promise<AutocompleteResult> {
189
+ // Ensure DuckDB is initialized
190
+ if (!isReady) {
191
+ try {
192
+ await initialize()
193
+ } catch (error) {
194
+ console.error('[DuckDB Plugin] Failed to initialize:', error)
195
+ return { type: 'options', options: [] }
196
+ }
197
+ }
198
+
199
+ if (!connection) {
200
+ return { type: 'options', options: [] }
201
+ }
202
+
203
+ if (!input.trim()) {
204
+ return { type: 'options', options: [] }
205
+ }
206
+
207
+ try {
208
+ // Replace :search placeholder with actual value
209
+ const preparedQuery = query.replace(/:search/g, input.replace(/'/g, "''"))
210
+
211
+ const result = await connection.query(preparedQuery)
212
+ const rows = result.toArray()
213
+
214
+ // Get column names
215
+ const columns = result.schema.fields.map((f: any) => f.name)
216
+ const valueColumn = columns[0]
217
+ const labelColumn = columns.length > 1 ? columns[1] : columns[0]
218
+
219
+ const options: AutocompleteOption[] = rows.map((row: any) => {
220
+ // Convert row to object
221
+ const rowObj: Record<string, any> = {}
222
+ columns.forEach((col: string, idx: number) => {
223
+ rowObj[col] = row[idx] ?? row[col]
224
+ })
225
+
226
+ return {
227
+ value: String(rowObj[valueColumn]),
228
+ label: String(rowObj[labelColumn]),
229
+ metadata: rowObj
230
+ }
231
+ })
232
+
233
+ return {
234
+ type: 'options',
235
+ options
236
+ }
237
+ } catch (error) {
238
+ console.error('[DuckDB Plugin] Query error:', error)
239
+ return { type: 'options', options: [] }
240
+ }
241
+ },
242
+
243
+ async dispose() {
244
+ if (connection) {
245
+ try {
246
+ await connection.close()
247
+ } catch (e) {
248
+ console.error('[DuckDB Plugin] Error closing connection:', e)
249
+ }
250
+ connection = null
251
+ }
252
+ isReady = false
253
+ initPromise = null
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Preload DuckDB WASM (call early to warm cache)
260
+ */
261
+ export async function preloadDuckDB(): Promise<void> {
262
+ try {
263
+ await loadDuckDB()
264
+ } catch (error) {
265
+ console.error('[DuckDB Plugin] Preload error:', error)
266
+ }
267
+ }
268
+
269
+ export default createDuckDBPlugin
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Groq LLM Autocomplete Plugin
3
+ * Provides LLM-powered text completion suggestions using Groq API
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ */
7
+
8
+ import type {
9
+ AutocompletePlugin,
10
+ AutocompleteResult,
11
+ AutocompleteContext,
12
+ GroqPluginConfig
13
+ } from '../types'
14
+
15
+ /**
16
+ * Default system prompt for completion
17
+ */
18
+ const DEFAULT_SYSTEM_PROMPT = `You are an autocomplete assistant. Given the user's partial input, provide a natural completion.
19
+ Rules:
20
+ - Complete the text naturally and concisely
21
+ - Return ONLY the completed text (including the original input)
22
+ - Do not add quotes, explanations, or additional text
23
+ - If unsure, return the original input unchanged`
24
+
25
+ /**
26
+ * Create a Groq LLM autocomplete plugin
27
+ */
28
+ export function createGroqPlugin(config: GroqPluginConfig): AutocompletePlugin {
29
+ const {
30
+ apiKey,
31
+ model = 'mixtral-8x7b-32768',
32
+ systemPrompt = DEFAULT_SYSTEM_PROMPT,
33
+ maxTokens = 50,
34
+ temperature = 0.3
35
+ } = config
36
+
37
+ let isConfigured = !!apiKey
38
+
39
+ return {
40
+ id: 'groq',
41
+ name: 'Groq LLM',
42
+
43
+ configure(newConfig: Record<string, any>) {
44
+ if (newConfig.apiKey) {
45
+ isConfigured = true
46
+ }
47
+ },
48
+
49
+ isReady() {
50
+ return isConfigured
51
+ },
52
+
53
+ async getSuggestions(
54
+ input: string,
55
+ context?: AutocompleteContext
56
+ ): Promise<AutocompleteResult> {
57
+ if (!isConfigured) {
58
+ console.warn('[Groq Plugin] API key not configured')
59
+ return { type: 'completion', completion: input }
60
+ }
61
+
62
+ if (!input.trim()) {
63
+ return { type: 'completion', completion: '' }
64
+ }
65
+
66
+ try {
67
+ // Build context-aware prompt
68
+ let userPrompt = `Complete this text: "${input}"`
69
+
70
+ if (context?.fieldName) {
71
+ userPrompt = `Field: ${context.fieldName}\nComplete this text: "${input}"`
72
+ }
73
+
74
+ if (context?.formData && Object.keys(context.formData).length > 0) {
75
+ const formContext = Object.entries(context.formData)
76
+ .filter(([key, value]) => value && key !== context.fieldName)
77
+ .map(([key, value]) => `${key}: ${value}`)
78
+ .join(', ')
79
+
80
+ if (formContext) {
81
+ userPrompt = `Form context: ${formContext}\n${userPrompt}`
82
+ }
83
+ }
84
+
85
+ const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'Authorization': `Bearer ${apiKey}`
90
+ },
91
+ body: JSON.stringify({
92
+ model,
93
+ messages: [
94
+ { role: 'system', content: systemPrompt },
95
+ { role: 'user', content: userPrompt }
96
+ ],
97
+ max_tokens: maxTokens,
98
+ temperature,
99
+ stream: false
100
+ })
101
+ })
102
+
103
+ if (!response.ok) {
104
+ const errorText = await response.text()
105
+ console.error('[Groq Plugin] API error:', response.status, errorText)
106
+ return { type: 'completion', completion: input }
107
+ }
108
+
109
+ const data = await response.json()
110
+ const completion = data.choices?.[0]?.message?.content?.trim() || input
111
+
112
+ // Clean up the completion (remove quotes if present)
113
+ let cleanCompletion = completion
114
+ if (cleanCompletion.startsWith('"') && cleanCompletion.endsWith('"')) {
115
+ cleanCompletion = cleanCompletion.slice(1, -1)
116
+ }
117
+ if (cleanCompletion.startsWith("'") && cleanCompletion.endsWith("'")) {
118
+ cleanCompletion = cleanCompletion.slice(1, -1)
119
+ }
120
+
121
+ return {
122
+ type: 'completion',
123
+ completion: cleanCompletion
124
+ }
125
+ } catch (error) {
126
+ console.error('[Groq Plugin] Error:', error)
127
+ return { type: 'completion', completion: input }
128
+ }
129
+ },
130
+
131
+ dispose() {
132
+ // No cleanup needed
133
+ }
134
+ }
135
+ }
136
+
137
+ export default createGroqPlugin
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Autocomplete Plugins Index
3
+ * Re-exports all available autocomplete plugins
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ */
7
+
8
+ export { createGroqPlugin } from './groq'
9
+ export { createSupabasePlugin } from './supabase'
10
+ export { createRestPlugin } from './rest'
11
+
12
+ // DuckDB plugin is exported separately due to WASM dependencies
13
+ // Import directly from './duckdb' when needed:
14
+ // import { createDuckDBPlugin, preloadDuckDB } from '@seed-ship/mcp-ui-solid/plugins/duckdb'
@@ -0,0 +1,147 @@
1
+ /**
2
+ * REST API Autocomplete Plugin
3
+ * Provides suggestions from any REST API endpoint
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ */
7
+
8
+ import type {
9
+ AutocompletePlugin,
10
+ AutocompleteResult,
11
+ AutocompleteContext,
12
+ AutocompleteOption,
13
+ RestPluginConfig
14
+ } from '../types'
15
+
16
+ /**
17
+ * Get value at path (e.g., "data.results" -> obj.data.results)
18
+ */
19
+ function getByPath(obj: any, path: string): any {
20
+ return path.split('.').reduce((current, key) => current?.[key], obj)
21
+ }
22
+
23
+ /**
24
+ * Default transform function
25
+ */
26
+ function defaultTransform(
27
+ data: any[],
28
+ valueField: string,
29
+ labelField: string
30
+ ): AutocompleteOption[] {
31
+ return data.map(item => ({
32
+ value: String(item[valueField] ?? item.value ?? item.id ?? ''),
33
+ label: String(item[labelField] ?? item.label ?? item.name ?? item[valueField] ?? ''),
34
+ metadata: item
35
+ }))
36
+ }
37
+
38
+ /**
39
+ * Create a REST API autocomplete plugin
40
+ */
41
+ export function createRestPlugin(config: RestPluginConfig): AutocompletePlugin {
42
+ const {
43
+ endpoint,
44
+ method = 'GET',
45
+ headers = {},
46
+ bodyTemplate,
47
+ transform,
48
+ resultPath,
49
+ valueField = 'value',
50
+ labelField = 'label'
51
+ } = config
52
+
53
+ return {
54
+ id: 'rest',
55
+ name: 'REST API',
56
+
57
+ configure(newConfig: Record<string, any>) {
58
+ Object.assign(config, newConfig)
59
+ },
60
+
61
+ isReady() {
62
+ return !!endpoint
63
+ },
64
+
65
+ async getSuggestions(
66
+ input: string,
67
+ _context?: AutocompleteContext
68
+ ): Promise<AutocompleteResult> {
69
+ if (!endpoint) {
70
+ console.warn('[REST Plugin] Endpoint not configured')
71
+ return { type: 'options', options: [] }
72
+ }
73
+
74
+ if (!input.trim()) {
75
+ return { type: 'options', options: [] }
76
+ }
77
+
78
+ try {
79
+ // Build URL with search placeholder
80
+ const url = endpoint.replace(/\{search\}/g, encodeURIComponent(input))
81
+
82
+ // Build request options
83
+ const requestOptions: RequestInit = {
84
+ method,
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ ...headers
88
+ }
89
+ }
90
+
91
+ // Add body for POST requests
92
+ if (method === 'POST' && bodyTemplate) {
93
+ const body = bodyTemplate.replace(/\{search\}/g, input)
94
+ requestOptions.body = body
95
+ } else if (method === 'POST') {
96
+ requestOptions.body = JSON.stringify({ search: input })
97
+ }
98
+
99
+ const response = await fetch(url, requestOptions)
100
+
101
+ if (!response.ok) {
102
+ const errorText = await response.text()
103
+ console.error('[REST Plugin] API error:', response.status, errorText)
104
+ return { type: 'options', options: [] }
105
+ }
106
+
107
+ const data = await response.json()
108
+
109
+ // Extract results from path if specified
110
+ let results = resultPath ? getByPath(data, resultPath) : data
111
+
112
+ // Ensure results is an array
113
+ if (!Array.isArray(results)) {
114
+ if (results && typeof results === 'object') {
115
+ // Try common result structures
116
+ results = results.data || results.results || results.items || [results]
117
+ } else {
118
+ results = []
119
+ }
120
+ }
121
+
122
+ // Transform results to options
123
+ let options: AutocompleteOption[]
124
+
125
+ if (transform) {
126
+ options = transform(results)
127
+ } else {
128
+ options = defaultTransform(results, valueField, labelField)
129
+ }
130
+
131
+ return {
132
+ type: 'options',
133
+ options
134
+ }
135
+ } catch (error) {
136
+ console.error('[REST Plugin] Error:', error)
137
+ return { type: 'options', options: [] }
138
+ }
139
+ },
140
+
141
+ dispose() {
142
+ // No cleanup needed
143
+ }
144
+ }
145
+ }
146
+
147
+ export default createRestPlugin
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Supabase Autocomplete Plugin
3
+ * Provides data-driven suggestions from a Supabase table
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ */
7
+
8
+ import type {
9
+ AutocompletePlugin,
10
+ AutocompleteResult,
11
+ AutocompleteContext,
12
+ AutocompleteOption,
13
+ SupabasePluginConfig
14
+ } from '../types'
15
+
16
+ /**
17
+ * Create a Supabase autocomplete plugin
18
+ */
19
+ export function createSupabasePlugin(config: SupabasePluginConfig): AutocompletePlugin {
20
+ const {
21
+ url,
22
+ anonKey,
23
+ table,
24
+ column,
25
+ searchColumn,
26
+ labelColumn,
27
+ limit = 10,
28
+ filter
29
+ } = config
30
+
31
+ const isConfigured = !!(url && anonKey && table && column)
32
+ const effectiveSearchColumn = searchColumn || column
33
+ const effectiveLabelColumn = labelColumn || column
34
+
35
+ return {
36
+ id: 'supabase',
37
+ name: 'Supabase',
38
+
39
+ configure(newConfig: Record<string, any>) {
40
+ // Allow runtime reconfiguration
41
+ Object.assign(config, newConfig)
42
+ },
43
+
44
+ isReady() {
45
+ return isConfigured
46
+ },
47
+
48
+ async getSuggestions(
49
+ input: string,
50
+ _context?: AutocompleteContext
51
+ ): Promise<AutocompleteResult> {
52
+ if (!isConfigured) {
53
+ console.warn('[Supabase Plugin] Not properly configured')
54
+ return { type: 'options', options: [] }
55
+ }
56
+
57
+ if (!input.trim()) {
58
+ return { type: 'options', options: [] }
59
+ }
60
+
61
+ try {
62
+ // Build query URL
63
+ let queryUrl = `${url}/rest/v1/${table}?select=${column}`
64
+
65
+ if (column !== effectiveLabelColumn) {
66
+ queryUrl += `,${effectiveLabelColumn}`
67
+ }
68
+
69
+ // Add ILIKE filter for search
70
+ queryUrl += `&${effectiveSearchColumn}=ilike.${encodeURIComponent(input)}%`
71
+
72
+ // Add limit
73
+ queryUrl += `&limit=${limit}`
74
+
75
+ // Add custom filters
76
+ if (filter) {
77
+ Object.entries(filter).forEach(([key, value]) => {
78
+ queryUrl += `&${key}=eq.${encodeURIComponent(String(value))}`
79
+ })
80
+ }
81
+
82
+ const response = await fetch(queryUrl, {
83
+ headers: {
84
+ 'apikey': anonKey,
85
+ 'Authorization': `Bearer ${anonKey}`,
86
+ 'Content-Type': 'application/json'
87
+ }
88
+ })
89
+
90
+ if (!response.ok) {
91
+ const errorText = await response.text()
92
+ console.error('[Supabase Plugin] API error:', response.status, errorText)
93
+ return { type: 'options', options: [] }
94
+ }
95
+
96
+ const data = await response.json()
97
+
98
+ const options: AutocompleteOption[] = data.map((row: Record<string, any>) => ({
99
+ value: String(row[column]),
100
+ label: String(row[effectiveLabelColumn]),
101
+ metadata: row
102
+ }))
103
+
104
+ return {
105
+ type: 'options',
106
+ options
107
+ }
108
+ } catch (error) {
109
+ console.error('[Supabase Plugin] Error:', error)
110
+ return { type: 'options', options: [] }
111
+ }
112
+ },
113
+
114
+ dispose() {
115
+ // No cleanup needed
116
+ }
117
+ }
118
+ }
119
+
120
+ export default createSupabasePlugin