|
<script lang="ts"> |
|
import CodeMirror from '$lib/CodeMirror/CodeMirror.svelte'; |
|
import { EditorView, lineNumbers } from '@codemirror/view'; |
|
import CopyButton from '$lib/CopyButton/CopyButton.svelte'; |
|
import { javascript } from '@codemirror/lang-javascript'; |
|
import { linter, lintGutter, type Diagnostic } from '@codemirror/lint'; |
|
import LineWrapButton from '$lib/LineWrapButton/LineWrapButton.svelte'; |
|
import JSON5 from 'json5'; |
|
import type { FormattedChatTemplate } from '../ChatTemplateViewer/types'; |
|
import { getExampleHelloWorld } from '$lib/example-inputs/helloWorld'; |
|
import { onMount } from 'svelte'; |
|
import { getExampleToolUsage } from '$lib/example-inputs/toolUsage'; |
|
import { foldGutter } from '@codemirror/language'; |
|
import { getExampleReasoning } from '$lib/example-inputs/reasoning'; |
|
import { tooltip } from '$lib/utils/tooltip'; |
|
import IconRestart from '$lib/Icons/IconRestart.svelte'; |
|
import { createEventDispatcher } from 'svelte'; |
|
import { page } from '$app/stores'; |
|
import { transformInput } from '$lib/utils/transformInput'; |
|
|
|
export let content: Record<string, unknown> = {}; |
|
export let error = ''; |
|
export let selectedTemplate: FormattedChatTemplate | undefined = undefined; |
|
export let selectedExampleInputId = ''; |
|
|
|
const dispatch = createEventDispatcher<{ exampleChange: void }>(); |
|
|
|
let value = JSON5.stringify(content, null, 2); |
|
let wrapLines = true; |
|
let exampleInputs: { id: string; label: string; content: Record<string, unknown> }[] = []; |
|
let exampleValue = ''; |
|
|
|
async function handleUpdateEditor(e: CustomEvent<string>) { |
|
const currentCode = e.detail; |
|
try { |
|
content = JSON5.parse(currentCode); |
|
value = currentCode; |
|
error = ''; |
|
} catch (e) { |
|
console.error(e); |
|
error = 'Error in input JSON'; |
|
} |
|
} |
|
|
|
function jsonLinter() { |
|
return (view: EditorView): Diagnostic[] => { |
|
const diagnostics: Diagnostic[] = []; |
|
const text = view.state.doc.toString(); |
|
|
|
try { |
|
// Attempt to parse the JSON5 |
|
JSON5.parse(text); |
|
} catch (e) { |
|
let pos = 0; |
|
let errorMessage = ''; |
|
|
|
if (e && typeof e === 'object') { |
|
errorMessage = e.message || ''; |
|
// Prefer JSON5 error properties if available |
|
const line = e.lineNumber; |
|
const column = e.columnNumber; |
|
if (typeof line === 'number' && typeof column === 'number') { |
|
// Convert line/column to character offset |
|
let runningPos = 0; |
|
let currentLine = 1; |
|
for (let i = 0; i < text.length; i++) { |
|
if (currentLine === line && runningPos === column - 1) { |
|
pos = i; |
|
break; |
|
} |
|
if (text[i] === '\n') { |
|
currentLine++; |
|
runningPos = 0; |
|
} else { |
|
runningPos++; |
|
} |
|
} |
|
} else { |
|
// Fallback: try to extract from message |
|
const match = /at position (\d+)/.exec(errorMessage); |
|
if (match) { |
|
pos = parseInt(match[1], 10); |
|
} else { |
|
const lineMatch = /line (\d+) column (\d+)/.exec(errorMessage); |
|
if (lineMatch) { |
|
const l = parseInt(lineMatch[1], 10); |
|
const c = parseInt(lineMatch[2], 10); |
|
let runningPos = 0; |
|
let currentLine = 1; |
|
for (let i = 0; i < text.length; i++) { |
|
if (currentLine === l && runningPos === c - 1) { |
|
pos = i; |
|
break; |
|
} |
|
if (text[i] === '\n') { |
|
currentLine++; |
|
runningPos = 0; |
|
} else { |
|
runningPos++; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
diagnostics.push({ |
|
from: Math.max(0, pos - 1), |
|
to: Math.min(text.length, pos + 1), |
|
severity: 'error', |
|
message: `JSON Error: ${errorMessage}` |
|
}); |
|
} |
|
|
|
return diagnostics; |
|
}; |
|
} |
|
|
|
function handleExampleInputChange(e: Event) { |
|
const target = e.target as HTMLSelectElement; |
|
const selectedId = target.value; |
|
const selectedExampleInput = exampleInputs.find( |
|
(exampleInput) => exampleInput.id === selectedId |
|
); |
|
if (selectedExampleInput) { |
|
selectedExampleInputId = selectedId; |
|
content = selectedExampleInput.content; |
|
value = JSON5.stringify(content, null, 2); |
|
exampleValue = value; |
|
dispatch('exampleChange'); |
|
} |
|
} |
|
|
|
onMount(() => { |
|
if (selectedTemplate) { |
|
const exampleHelloWorld = getExampleHelloWorld(selectedTemplate.template); |
|
if (exampleHelloWorld) { |
|
exampleInputs = [ |
|
...exampleInputs, |
|
{ |
|
id: 'hello-world', |
|
label: 'hello world example', |
|
content: transformInput(exampleHelloWorld, selectedTemplate.template) |
|
} |
|
]; |
|
} |
|
|
|
const exampleReasoning = getExampleReasoning(selectedTemplate.template); |
|
if (exampleReasoning) { |
|
exampleInputs = [ |
|
...exampleInputs, |
|
{ |
|
id: 'reasoning', |
|
label: 'reasoning example', |
|
content: transformInput(exampleReasoning, selectedTemplate.template) |
|
} |
|
]; |
|
} |
|
|
|
const exampleToolUsage = getExampleToolUsage(selectedTemplate.template); |
|
if (exampleToolUsage) { |
|
exampleInputs = [ |
|
...exampleInputs, |
|
{ |
|
id: 'tool-usage', |
|
label: 'tool usage example', |
|
content: transformInput(exampleToolUsage, selectedTemplate.template) |
|
} |
|
]; |
|
} |
|
|
|
const exampleFromQuery = $page.url.searchParams.get('example'); |
|
if (exampleFromQuery) { |
|
const exampleInput = exampleInputs.find( |
|
(exampleInput) => exampleInput.id === exampleFromQuery |
|
); |
|
if (exampleInput) { |
|
content = exampleInput.content; |
|
value = JSON5.stringify(content, null, 2); |
|
exampleValue = value; |
|
selectedExampleInputId = exampleInput.id; |
|
return; |
|
} |
|
} |
|
|
|
content = exampleInputs.at(-1)?.content ?? {}; |
|
selectedExampleInputId = exampleInputs.at(-1)?.id ?? ''; |
|
value = JSON5.stringify(content, null, 2); |
|
exampleValue = value; |
|
} |
|
}); |
|
</script> |
|
|
|
<div class="h-full overflow-scroll bg-white dark:bg-gray-900"> |
|
<div class="sticky top-0 z-10 bg-white dark:bg-gray-900"> |
|
<div |
|
class="text-semibold flex items-center gap-x-2 border-b border-gray-500 bg-linear-to-r from-orange-200 to-white px-3 py-1.5 text-lg dark:from-orange-700 dark:to-orange-900 dark:text-gray-200" |
|
> |
|
JSON Input |
|
|
|
{#if exampleInputs.length > 1} |
|
<select |
|
class="ml-auto rounded border px-1 py-0.5 text-sm" |
|
on:change={handleExampleInputChange} |
|
> |
|
{#each exampleInputs as exampleInput} |
|
<option value={exampleInput.id} selected={exampleInput.id === selectedExampleInputId} |
|
>{exampleInput.label}</option |
|
> |
|
{/each} |
|
</select> |
|
{/if} |
|
</div> |
|
<div class="flex items-center border-b px-3 py-2"> |
|
<div class="ml-auto flex items-center gap-x-2"> |
|
|
|
{#if exampleValue && exampleValue !== value} |
|
<button |
|
class="relative inline-flex h-6! cursor-pointer items-center justify-center rounded-md border border-gray-500 bg-white p-0! px-1.5! text-sm shadow-xs focus:outline-hidden dark:bg-gray-900 dark:text-white [&_svg]:translate-x-px! [&_svg]:translate-y-px! [&_svg]:text-base!" |
|
type="button" |
|
on:click={() => { |
|
value = exampleValue; |
|
}} |
|
use:tooltip={'Reset example to original'} |
|
><IconRestart classNames="dark:text-gray-200!" /> |
|
<span class="ml-1 text-sm select-none dark:text-gray-200!"> Reset </span> |
|
</button> |
|
{/if} |
|
|
|
<CopyButton |
|
label="Copy" |
|
{value} |
|
style="button-clear" |
|
classNames="h-6! [&_svg]:text-[0.7rem]! px-1.5! text-black! dark:text-gray-200!" |
|
/> |
|
|
|
<LineWrapButton |
|
style="button-clear" |
|
bind:wrapLines |
|
classNames="[&_svg]:text-xs! size-6! p-0!" |
|
/> |
|
</div> |
|
</div> |
|
</div> |
|
<CodeMirror |
|
{value} |
|
on:change={handleUpdateEditor} |
|
extensions={[ |
|
lineNumbers(), |
|
javascript({ jsx: false, typescript: false }), |
|
linter(jsonLinter()), |
|
lintGutter(), |
|
foldGutter(), |
|
...[wrapLines ? [EditorView.lineWrapping] : []] |
|
]} |
|
/> |
|
</div> |
|
|