mirror of
https://github.com/voson-wang/toon.git
synced 2026-01-29 15:24:10 +08:00
docs: add JSON baseline format selector to playground
This commit is contained in:
@@ -7,8 +7,11 @@ import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
|||||||
import { DEFAULT_DELIMITER, encode } from '../../../../packages/toon/src'
|
import { DEFAULT_DELIMITER, encode } from '../../../../packages/toon/src'
|
||||||
import VPInput from './VPInput.vue'
|
import VPInput from './VPInput.vue'
|
||||||
|
|
||||||
|
type JsonFormat = 'pretty-2' | 'pretty-4' | 'pretty-tab' | 'compact'
|
||||||
|
|
||||||
interface PlaygroundState extends Required<Pick<EncodeOptions, 'delimiter' | 'indent' | 'keyFolding' | 'flattenDepth'>> {
|
interface PlaygroundState extends Required<Pick<EncodeOptions, 'delimiter' | 'indent' | 'keyFolding' | 'flattenDepth'>> {
|
||||||
json: string
|
json: string
|
||||||
|
jsonFormat: JsonFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRESETS = {
|
const PRESETS = {
|
||||||
@@ -70,20 +73,37 @@ const DELIMITER_OPTIONS: { value: Delimiter, label: string }[] = [
|
|||||||
{ value: '\t', label: 'Tab (\\t)' },
|
{ value: '\t', label: 'Tab (\\t)' },
|
||||||
{ value: '|', label: 'Pipe (|)' },
|
{ value: '|', label: 'Pipe (|)' },
|
||||||
]
|
]
|
||||||
|
const JSON_FORMAT_OPTIONS: { value: JsonFormat, label: string, indent: string | number | undefined }[] = [
|
||||||
|
{ value: 'pretty-2', label: 'Pretty (2 spaces)', indent: 2 },
|
||||||
|
{ value: 'pretty-4', label: 'Pretty (4 spaces)', indent: 4 },
|
||||||
|
{ value: 'pretty-tab', label: 'Pretty (tabs)', indent: '\t' },
|
||||||
|
{ value: 'compact', label: 'Compact', indent: undefined },
|
||||||
|
]
|
||||||
const DEFAULT_JSON = JSON.stringify(PRESETS.hikes, undefined, 2)
|
const DEFAULT_JSON = JSON.stringify(PRESETS.hikes, undefined, 2)
|
||||||
const SHARE_URL_LIMIT = 8 * 1024
|
const SHARE_URL_LIMIT = 8 * 1024
|
||||||
|
|
||||||
|
// Input state
|
||||||
const jsonInput = ref(DEFAULT_JSON)
|
const jsonInput = ref(DEFAULT_JSON)
|
||||||
|
const jsonFormat = ref<JsonFormat>('pretty-2')
|
||||||
|
const currentFormatIndent = computed(() =>
|
||||||
|
JSON_FORMAT_OPTIONS.find(opt => opt.value === jsonFormat.value)?.indent,
|
||||||
|
)
|
||||||
|
const formattedJson = computed(() => {
|
||||||
|
try {
|
||||||
|
return formatJson(JSON.parse(jsonInput.value))
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return jsonInput.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Encoder options
|
||||||
const delimiter = ref<Delimiter>(DEFAULT_DELIMITER)
|
const delimiter = ref<Delimiter>(DEFAULT_DELIMITER)
|
||||||
const indent = ref(2)
|
const indent = ref(2)
|
||||||
const keyFolding = ref<'off' | 'safe'>('safe')
|
const keyFolding = ref<'off' | 'safe'>('safe')
|
||||||
const flattenDepth = ref(2)
|
const flattenDepth = ref(2)
|
||||||
|
|
||||||
const canShareState = ref(true)
|
// Encoding output
|
||||||
const hasCopiedUrl = ref(false)
|
|
||||||
|
|
||||||
const tokenizer = shallowRef<typeof import('gpt-tokenizer') | undefined>()
|
|
||||||
|
|
||||||
const encodingResult = computed(() => {
|
const encodingResult = computed(() => {
|
||||||
try {
|
try {
|
||||||
const parsedInput = JSON.parse(jsonInput.value)
|
const parsedInput = JSON.parse(jsonInput.value)
|
||||||
@@ -104,12 +124,13 @@ const encodingResult = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const toonOutput = computed(() => encodingResult.value.output)
|
const toonOutput = computed(() => encodingResult.value.output)
|
||||||
const error = computed(() => encodingResult.value.error)
|
const error = computed(() => encodingResult.value.error)
|
||||||
|
|
||||||
|
// Token analysis
|
||||||
|
const tokenizer = shallowRef<typeof import('gpt-tokenizer') | undefined>()
|
||||||
const jsonTokens = computed(() =>
|
const jsonTokens = computed(() =>
|
||||||
tokenizer.value?.encode(jsonInput.value).length,
|
tokenizer.value?.encode(formattedJson.value).length,
|
||||||
)
|
)
|
||||||
const toonTokens = computed(() =>
|
const toonTokens = computed(() =>
|
||||||
tokenizer.value && toonOutput.value ? tokenizer.value.encode(toonOutput.value).length : undefined,
|
tokenizer.value && toonOutput.value ? tokenizer.value.encode(toonOutput.value).length : undefined,
|
||||||
@@ -125,17 +146,11 @@ const tokenSavings = computed(() => {
|
|||||||
return { diff, percent, sign, isSavings: diff > 0 }
|
return { diff, percent, sign, isSavings: diff > 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const canShareState = ref(true)
|
||||||
|
const hasCopiedUrl = ref(false)
|
||||||
|
|
||||||
const { copy, copied } = useClipboard({ source: toonOutput })
|
const { copy, copied } = useClipboard({ source: toonOutput })
|
||||||
|
|
||||||
async function copyShareUrl() {
|
|
||||||
if (!canShareState.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
await navigator.clipboard.writeText(window.location.href)
|
|
||||||
hasCopiedUrl.value = true
|
|
||||||
setTimeout(() => (hasCopiedUrl.value = false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUrl = useDebounceFn(() => {
|
const updateUrl = useDebounceFn(() => {
|
||||||
const hash = encodeState()
|
const hash = encodeState()
|
||||||
const baseUrl = `${window.location.origin}${window.location.pathname}${window.location.search}`
|
const baseUrl = `${window.location.origin}${window.location.pathname}${window.location.search}`
|
||||||
@@ -150,10 +165,17 @@ const updateUrl = useDebounceFn(() => {
|
|||||||
window.history.replaceState(null, '', `#${hash}`)
|
window.history.replaceState(null, '', `#${hash}`)
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
watch([jsonInput, delimiter, indent, keyFolding, flattenDepth], () => {
|
watch([jsonInput, delimiter, indent, keyFolding, flattenDepth, jsonFormat], () => {
|
||||||
updateUrl()
|
updateUrl()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(jsonFormat, () => {
|
||||||
|
try {
|
||||||
|
jsonInput.value = formatJson(JSON.parse(jsonInput.value))
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTokenizer()
|
loadTokenizer()
|
||||||
|
|
||||||
@@ -168,9 +190,14 @@ onMounted(() => {
|
|||||||
indent.value = state.indent
|
indent.value = state.indent
|
||||||
keyFolding.value = state.keyFolding ?? 'safe'
|
keyFolding.value = state.keyFolding ?? 'safe'
|
||||||
flattenDepth.value = state.flattenDepth ?? 2
|
flattenDepth.value = state.flattenDepth ?? 2
|
||||||
|
jsonFormat.value = state.jsonFormat ?? 'pretty-2'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function formatJson(value: unknown) {
|
||||||
|
return JSON.stringify(value, undefined, currentFormatIndent.value)
|
||||||
|
}
|
||||||
|
|
||||||
function encodeState() {
|
function encodeState() {
|
||||||
const state: PlaygroundState = {
|
const state: PlaygroundState = {
|
||||||
json: jsonInput.value,
|
json: jsonInput.value,
|
||||||
@@ -178,6 +205,7 @@ function encodeState() {
|
|||||||
indent: indent.value,
|
indent: indent.value,
|
||||||
keyFolding: keyFolding.value,
|
keyFolding: keyFolding.value,
|
||||||
flattenDepth: flattenDepth.value,
|
flattenDepth: flattenDepth.value,
|
||||||
|
jsonFormat: jsonFormat.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
const compressedData = zlibSync(stringToUint8Array(JSON.stringify(state)))
|
const compressedData = zlibSync(stringToUint8Array(JSON.stringify(state)))
|
||||||
@@ -196,7 +224,16 @@ function decodeState(hash: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPreset(name: keyof typeof PRESETS) {
|
function loadPreset(name: keyof typeof PRESETS) {
|
||||||
jsonInput.value = JSON.stringify(PRESETS[name], undefined, 2)
|
jsonInput.value = formatJson(PRESETS[name])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyShareUrl() {
|
||||||
|
if (!canShareState.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(window.location.href)
|
||||||
|
hasCopiedUrl.value = true
|
||||||
|
setTimeout(() => (hasCopiedUrl.value = false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTokenizer() {
|
async function loadTokenizer() {
|
||||||
@@ -258,7 +295,7 @@ async function loadTokenizer() {
|
|||||||
<VPInput id="preset" label="Preset">
|
<VPInput id="preset" label="Preset">
|
||||||
<select id="preset" @change="(e) => loadPreset((e.target as HTMLSelectElement).value as keyof typeof PRESETS)">
|
<select id="preset" @change="(e) => loadPreset((e.target as HTMLSelectElement).value as keyof typeof PRESETS)">
|
||||||
<option value="" disabled selected>
|
<option value="" disabled selected>
|
||||||
Load example...
|
Load example…
|
||||||
</option>
|
</option>
|
||||||
<option value="hikes">
|
<option value="hikes">
|
||||||
Hikes (mixed structure)
|
Hikes (mixed structure)
|
||||||
@@ -275,6 +312,14 @@ async function loadTokenizer() {
|
|||||||
</select>
|
</select>
|
||||||
</VPInput>
|
</VPInput>
|
||||||
|
|
||||||
|
<VPInput id="jsonFormat" label="JSON Baseline">
|
||||||
|
<select id="jsonFormat" v-model="jsonFormat">
|
||||||
|
<option v-for="opt in JSON_FORMAT_OPTIONS" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</VPInput>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="share-button"
|
class="share-button"
|
||||||
:class="[hasCopiedUrl && 'copied']"
|
:class="[hasCopiedUrl && 'copied']"
|
||||||
@@ -307,8 +352,8 @@ async function loadTokenizer() {
|
|||||||
<div class="pane-header">
|
<div class="pane-header">
|
||||||
<span class="pane-title">JSON Input</span>
|
<span class="pane-title">JSON Input</span>
|
||||||
<span class="pane-stats">
|
<span class="pane-stats">
|
||||||
<span>{{ jsonTokens ?? '...' }} tokens</span>
|
<span class="stat-primary" title="Token count using selected JSON baseline format">{{ jsonTokens ?? '…' }} tokens</span>
|
||||||
<span>{{ jsonInput.length }} chars</span>
|
<span class="stat-secondary">{{ formattedJson.length }} chars</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -319,7 +364,7 @@ async function loadTokenizer() {
|
|||||||
aria-label="JSON input"
|
aria-label="JSON input"
|
||||||
:aria-describedby="error ? 'json-error' : undefined"
|
:aria-describedby="error ? 'json-error' : undefined"
|
||||||
:aria-invalid="!!error"
|
:aria-invalid="!!error"
|
||||||
placeholder="Enter JSON here..."
|
placeholder="Enter JSON here…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,8 +378,8 @@ async function loadTokenizer() {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="pane-stats">
|
<span class="pane-stats">
|
||||||
<span>{{ toonTokens ?? '...' }} tokens</span>
|
<span class="stat-primary">{{ toonTokens ?? '…' }} tokens</span>
|
||||||
<span>{{ toonOutput.length }} chars</span>
|
<span class="stat-secondary">{{ toonOutput.length }} chars</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-output">
|
<div class="editor-output">
|
||||||
@@ -541,6 +586,15 @@ async function loadTokenizer() {
|
|||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-primary {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-secondary {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
.savings-badge {
|
.savings-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user