Generated by Spark: Create new snippet and Edit snippet should allow you to configure the input parameters, incl what function gets run (function name) and function arguments.

This commit is contained in:
2026-01-17 17:23:16 +00:00
committed by GitHub
parent 8c72d6b713
commit 5b395dcfc0
5 changed files with 248 additions and 20 deletions

View File

@@ -4,13 +4,16 @@ import * as ReactDOM from 'react-dom'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AIErrorHelper } from '@/components/AIErrorHelper'
import { WarningCircle } from '@phosphor-icons/react'
import { InputParameter } from '@/lib/types'
interface ReactPreviewProps {
code: string
language: string
functionName?: string
inputParameters?: InputParameter[]
}
export function ReactPreview({ code, language }: ReactPreviewProps) {
export function ReactPreview({ code, language, functionName, inputParameters }: ReactPreviewProps) {
const [error, setError] = useState<string | null>(null)
const [Component, setComponent] = useState<React.ComponentType | null>(null)
const mountRef = useRef<HTMLDivElement>(null)
@@ -43,8 +46,10 @@ export function ReactPreview({ code, language }: ReactPreviewProps) {
${transformedCode}
${functionName ? `return ${functionName};` : `
const lastStatement = (${transformedCode.trim().split('\n').pop()});
return lastStatement;
`}
})
`
@@ -61,7 +66,34 @@ export function ReactPreview({ code, language }: ReactPreviewProps) {
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to render preview')
}
}, [code, language])
}, [code, language, functionName])
const props = React.useMemo(() => {
if (!inputParameters || inputParameters.length === 0) {
return {}
}
const parsedProps: Record<string, any> = {}
inputParameters.forEach((param) => {
try {
if (param.type === 'string') {
parsedProps[param.name] = param.defaultValue.replace(/^["']|["']$/g, '')
} else if (param.type === 'number') {
parsedProps[param.name] = Number(param.defaultValue)
} else if (param.type === 'boolean') {
parsedProps[param.name] = param.defaultValue === 'true'
} else if (param.type === 'array' || param.type === 'object') {
parsedProps[param.name] = JSON.parse(param.defaultValue)
}
} catch (err) {
console.warn(`Failed to parse parameter ${param.name}:`, err)
parsedProps[param.name] = param.defaultValue
}
})
return parsedProps
}, [inputParameters])
if (!['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)) {
return (
@@ -105,7 +137,7 @@ export function ReactPreview({ code, language }: ReactPreviewProps) {
return (
<div className="h-full overflow-auto bg-background">
<div className="p-6" ref={mountRef}>
<Component />
<Component {...props} />
</div>
</div>
)

View File

@@ -19,10 +19,12 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Snippet } from '@/lib/types'
import { Snippet, InputParameter } from '@/lib/types'
import { MonacoEditor } from '@/components/MonacoEditor'
import { SplitScreenEditor } from '@/components/SplitScreenEditor'
import { strings, appConfig, LANGUAGES } from '@/lib/config'
import { Plus, Trash } from '@phosphor-icons/react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface SnippetDialogProps {
open: boolean
@@ -37,6 +39,8 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
const [language, setLanguage] = useState(appConfig.defaultLanguage)
const [code, setCode] = useState('')
const [hasPreview, setHasPreview] = useState(false)
const [functionName, setFunctionName] = useState('')
const [inputParameters, setInputParameters] = useState<InputParameter[]>([])
const [errors, setErrors] = useState<{ title?: string; code?: string }>({})
useEffect(() => {
@@ -46,12 +50,16 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
setLanguage(editingSnippet.language)
setCode(editingSnippet.code)
setHasPreview(editingSnippet.hasPreview || false)
setFunctionName(editingSnippet.functionName || '')
setInputParameters(editingSnippet.inputParameters || [])
} else {
setTitle('')
setDescription('')
setLanguage(appConfig.defaultLanguage)
setCode('')
setHasPreview(false)
setFunctionName('')
setInputParameters([])
}
setErrors({})
}, [editingSnippet, open])
@@ -78,6 +86,8 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
code: code.trim(),
category: editingSnippet?.category || 'general',
hasPreview,
functionName: functionName.trim() || undefined,
inputParameters: inputParameters.length > 0 ? inputParameters : undefined,
})
setTitle('')
@@ -85,10 +95,31 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
setLanguage(appConfig.defaultLanguage)
setCode('')
setHasPreview(false)
setFunctionName('')
setInputParameters([])
setErrors({})
onOpenChange(false)
}
const handleAddParameter = () => {
setInputParameters((prev) => [
...prev,
{ name: '', type: 'string', defaultValue: '', description: '' }
])
}
const handleRemoveParameter = (index: number) => {
setInputParameters((prev) => prev.filter((_, i) => i !== index))
}
const handleUpdateParameter = (index: number, field: keyof InputParameter, value: string) => {
setInputParameters((prev) =>
prev.map((param, i) =>
i === index ? { ...param, [field]: value } : param
)
)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-hidden flex flex-col">
@@ -152,6 +183,140 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
</div>
)}
{hasPreview && appConfig.previewEnabledLanguages.includes(language) && (
<Card className="bg-muted/30">
<CardHeader className="pb-3">
<CardTitle className="text-base font-semibold flex items-center justify-between">
<span>Preview Configuration</span>
<Button
variant="outline"
size="sm"
onClick={handleAddParameter}
className="gap-2"
>
<Plus className="h-3 w-3" />
Add Parameter
</Button>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="functionName" className="text-sm">
Function/Component Name (Optional)
</Label>
<Input
id="functionName"
placeholder="e.g., MyComponent"
value={functionName}
onChange={(e) => setFunctionName(e.target.value)}
className="bg-background"
/>
<p className="text-xs text-muted-foreground">
The name of the function or component to render. Leave empty to use the default export.
</p>
</div>
{inputParameters.length > 0 && (
<div className="space-y-3">
<Label className="text-sm font-medium">Input Parameters (Props)</Label>
{inputParameters.map((param, index) => (
<Card key={index} className="bg-background">
<CardContent className="pt-4 space-y-3">
<div className="flex items-start gap-2">
<div className="flex-1 grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label htmlFor={`param-name-${index}`} className="text-xs">
Name *
</Label>
<Input
id={`param-name-${index}`}
placeholder="paramName"
value={param.name}
onChange={(e) =>
handleUpdateParameter(index, 'name', e.target.value)
}
className="h-8 text-sm"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor={`param-type-${index}`} className="text-xs">
Type
</Label>
<Select
value={param.type}
onValueChange={(value) =>
handleUpdateParameter(index, 'type', value)
}
>
<SelectTrigger id={`param-type-${index}`} className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
<SelectItem value="array">array</SelectItem>
<SelectItem value="object">object</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveParameter(index)}
className="h-8 w-8 p-0 mt-6 text-destructive hover:text-destructive"
>
<Trash className="h-4 w-4" />
</Button>
</div>
<div className="space-y-1.5">
<Label htmlFor={`param-default-${index}`} className="text-xs">
Default Value *
</Label>
<Input
id={`param-default-${index}`}
placeholder={
param.type === 'string'
? '"Hello World"'
: param.type === 'number'
? '42'
: param.type === 'boolean'
? 'true'
: param.type === 'array'
? '["item1", "item2"]'
: '{"key": "value"}'
}
value={param.defaultValue}
onChange={(e) =>
handleUpdateParameter(index, 'defaultValue', e.target.value)
}
className="h-8 text-sm font-mono"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor={`param-desc-${index}`} className="text-xs">
Description (Optional)
</Label>
<Input
id={`param-desc-${index}`}
placeholder="What does this parameter do?"
value={param.description || ''}
onChange={(e) =>
handleUpdateParameter(index, 'description', e.target.value)
}
className="h-8 text-sm"
/>
</div>
</CardContent>
</Card>
))}
</div>
)}
</CardContent>
</Card>
)}
<div className="space-y-2">
<Label htmlFor="description">{strings.snippetDialog.fields.description.label}</Label>
<Textarea
@@ -174,6 +339,8 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
onChange={setCode}
language={language}
height="500px"
functionName={functionName}
inputParameters={inputParameters}
/>
</div>
) : (

View File

@@ -134,7 +134,12 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
/>
</div>
<div className="flex-1 overflow-hidden">
<ReactPreview code={snippet.code} language={snippet.language} />
<ReactPreview
code={snippet.code}
language={snippet.language}
functionName={snippet.functionName}
inputParameters={snippet.inputParameters}
/>
</div>
</>
) : (

View File

@@ -4,12 +4,15 @@ import { ReactPreview } from '@/components/ReactPreview'
import { Button } from '@/components/ui/button'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
import { Code, Eye, SplitHorizontal } from '@phosphor-icons/react'
import { InputParameter } from '@/lib/types'
interface SplitScreenEditorProps {
value: string
onChange: (value: string) => void
language: string
height?: string
functionName?: string
inputParameters?: InputParameter[]
}
type ViewMode = 'split' | 'code' | 'preview'
@@ -18,7 +21,9 @@ export function SplitScreenEditor({
value,
onChange,
language,
height = '500px'
height = '500px',
functionName,
inputParameters,
}: SplitScreenEditorProps) {
const [viewMode, setViewMode] = useState<ViewMode>('split')
@@ -83,7 +88,12 @@ export function SplitScreenEditor({
)}
{viewMode === 'preview' && (
<ReactPreview code={value} language={language} />
<ReactPreview
code={value}
language={language}
functionName={functionName}
inputParameters={inputParameters}
/>
)}
{viewMode === 'split' && (
@@ -98,7 +108,12 @@ export function SplitScreenEditor({
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50} minSize={30}>
<ReactPreview code={value} language={language} />
<ReactPreview
code={value}
language={language}
functionName={functionName}
inputParameters={inputParameters}
/>
</ResizablePanel>
</ResizablePanelGroup>
)}

View File

@@ -1,15 +1,24 @@
export type AtomicLevel = 'atoms' | 'molecules' | 'organisms' | 'templates'
export interface Snippet {
id: string
title: string
description: string
code: string
language: string
category: string
hasPreview?: boolean
createdAt: number
updatedAt: number
export type AtomicLevel = 'atoms' | 'molecules' | 'organisms' | 'templates'
export interface InputParameter {
name: string
type: 'string' | 'number' | 'boolean' | 'array' | 'object'
defaultValue: string
description?: string
}
export interface Snippet {
id: string
title: string
description: string
code: string
language: string
category: string
hasPreview?: boolean
functionName?: string
inputParameters?: InputParameter[]
createdAt: number
updatedAt: number
}