File size: 8,894 Bytes
6426ece
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
<script lang="ts">
	import JsonEditor from '$lib/JsonEditor/JsonEditor.svelte';
	import ChatTemplateViewer from '$lib/ChatTemplateViewer/ChatTemplateViewer.svelte';
	import OutputViewer from '$lib/OutputViewer/OutputViewer.svelte';
	import type { ChatTemplate, FormattedChatTemplate } from '$lib/ChatTemplateViewer/types';
	import { onMount } from 'svelte';
	import { Template } from '@huggingface/jinja';
	import { goto } from '$app/navigation';
	import { page } from '$app/stores';

	let modelId = $page.url.searchParams.get('modelId') ?? 'Qwen/Qwen3-235B-A22B';
	let formattedTemplates: FormattedChatTemplate[] = [];
	let selectedTemplate: FormattedChatTemplate | undefined = undefined;
	let showFormattedTemplate = true;
	let selectedExampleInputId = '';

	let leftWidth = 50; // percent
	let isDraggingVertical = false;

	let topHeight = 50; // percent (for right pane)
	let isDraggingHorizontal = false;

	let error = '';
	let output = '';

	let input = {
		messages: [
			{
				role: 'user',
				content: 'Hello, how are you?'
			},
			{
				role: 'assistant',
				content: "I'm doing great. How can I help you today?"
			},
			{
				role: 'user',
				content: 'Can you tell me a joke?'
			}
		],
		add_generation_prompt: true
	};

	$: {
		try {
			if (!input.messages) {
				error = "Invalid JSON: missing 'messages' key";
			}

			if (selectedTemplate) {
				const template = new Template(
					showFormattedTemplate ? selectedTemplate.formattedTemplate : selectedTemplate.template
				);
				output = template.render(input);
				error = '';
			}
		} catch (e) {
			error = e instanceof Error ? e.message : 'Unknown error';
		}
	}

	function startDragVertical(e: MouseEvent) {
		isDraggingVertical = true;
		document.body.style.cursor = 'col-resize';
	}

	function stopDragVertical() {
		isDraggingVertical = false;
		document.body.style.cursor = '';
	}

	function onDragVertical(e: MouseEvent) {
		if (!isDraggingVertical) return;
		const playground = document.getElementById('playground-container');
		if (!playground) return;
		const rect = playground.getBoundingClientRect();
		const offsetX = e.clientX - rect.left;
		let percent = (offsetX / rect.width) * 100;
		if (percent < 10) percent = 10;
		if (percent > 90) percent = 90;
		leftWidth = percent;
	}

	function startDragHorizontal(e: MouseEvent) {
		isDraggingHorizontal = true;
		document.body.style.cursor = 'row-resize';
	}

	function stopDragHorizontal() {
		isDraggingHorizontal = false;
		document.body.style.cursor = '';
	}

	function onDragHorizontal(e: MouseEvent) {
		if (!isDraggingHorizontal) return;
		const rightPane = document.getElementById('right-pane');
		if (!rightPane) return;
		const rect = rightPane.getBoundingClientRect();
		const offsetY = e.clientY - rect.top;
		let percent = (offsetY / rect.height) * 100;
		if (percent < 10) percent = 10;
		if (percent > 90) percent = 90;
		topHeight = percent;
	}

	async function getChatTemplate(modelId: string) {
		try {
			const res = await fetch('https://huggingface.co/api/models/' + modelId);

			if (!res.ok) {
				alert(`Failed to fetch model "${modelId}": ${res.status} ${res.statusText}`);
				return;
			}

			const model = await res.json();

			let chatTemplate: ChatTemplate | undefined = undefined;

			if (model.config?.chat_template_jinja) {
				//  model.config.chat_template_jinja & optional model.config.additional_chat_templates
				chatTemplate = model.config.chat_template_jinja;
				if (model.config?.additional_chat_templates) {
					chatTemplate = [
						{
							name: 'default',
							template: model.config.chat_template_jinja
						},
						...(model.config?.additional_chat_templates
							? Object.keys(model.config.additional_chat_templates).map((name) => ({
									name,
									template: model.config?.additional_chat_templates?.[name] ?? ''
								}))
							: [])
					];
				}
			} else if (model.config?.processor_config?.chat_template) {
				// for backward compatibility VLM
				chatTemplate = model.config.processor_config.chat_template;
			} else if (model.config?.tokenizer_config?.chat_template) {
				// for backward compatibility
				chatTemplate = model.config.tokenizer_config.chat_template;
			} else if (model.gguf?.chat_template) {
				// for GGUF models
				chatTemplate = model.gguf.chat_template;
			}

			const formattedTemplates: FormattedChatTemplate[] = (
				typeof chatTemplate === 'string'
					? [{ name: 'default', template: chatTemplate }]
					: chatTemplate
			) // Convert string template to array for unified handling
				.map(({ name, template }) => ({
					name,
					template,
					formattedTemplate: (() => {
						try {
							return new Template(template).format();
						} catch (error) {
							console.error(`Error formatting chat template ${name}:`, error);
							return template; // Return the original template in case of an error
						}
					})()
				})) // add formatted template attribute
				.map(({ name, template, formattedTemplate }) => ({
					name,
					template,
					formattedTemplate,
					templateUnedited: template,
					formattedTemplateUnedited: formattedTemplate
				}));

			let selectedTemplate =
				formattedTemplates.find(({ name }) => name === $page.url.searchParams.get('template')) ??
				formattedTemplates[0];

			return { formattedTemplates, selectedTemplate, model };
		} catch (error) {
			console.error(error);
		}
	}

	async function handleModelIdChange(newModelId: string, opts?: { replaceState?: boolean }) {
		const modelTemplate = await getChatTemplate(newModelId);
		if (modelTemplate) {
			modelId = newModelId;
			formattedTemplates = modelTemplate.formattedTemplates;
			selectedTemplate = modelTemplate.selectedTemplate;
			const model = modelTemplate.model;
			input = {
				...input,
				bos_token: model?.config?.tokenizer_config?.bos_token?.content ?? model?.gguf?.bos_token,
				eos_token: model?.config?.tokenizer_config?.eos_token?.content ?? model?.gguf?.eos_token,
				pad_token: model?.config?.tokenizer_config?.pad_token?.content ?? model?.gguf?.pad_token,
				unk_token: model?.config?.tokenizer_config?.unk_token?.content ?? model?.gguf?.unk_token
			};

			if (opts?.replaceState) {
				updateParams();
			}
		}
	}

	function updateParams() {
		let searchParams = '?modelId=' + modelId;
		if (selectedTemplate && selectedTemplate.name !== 'default') {
			searchParams += '&template=' + selectedTemplate.name;
		}
		if (selectedExampleInputId) {
			searchParams += '&example=' + selectedExampleInputId;
		}

		goto(searchParams, { replaceState: true });

		// post message to parent
		const parentOrigin = 'https://huggingface.co';
		window.parent.postMessage({ queryString: searchParams }, parentOrigin);
	}

	onMount(async () => {
		await handleModelIdChange(modelId);
	});
</script>

<svelte:window
	on:mousemove={onDragVertical}
	on:mouseup={stopDragVertical}
	on:mousemove={onDragHorizontal}
	on:mouseup={stopDragHorizontal}
/>

<div
	id="playground-container"
	class="relative flex h-screen w-full overflow-hidden border bg-white shadow select-none dark:bg-gray-950"
>
	<div class="overflow-auto" style="width: {leftWidth}%">
		{#if formattedTemplates.length}
			<ChatTemplateViewer
				{modelId}
				{formattedTemplates}
				bind:selectedTemplate
				bind:showFormattedTemplate
				on:modelIdChange={(e) => handleModelIdChange(e.detail, { replaceState: true })}
				on:templateChange={(e) => updateParams()}
			/>
		{/if}
	</div>

	<!-- svelte-ignore a11y_no_static_element_interactions -->
	<div
		class="hidden h-full w-1 cursor-col-resize items-center justify-center bg-gray-100 select-none hover:bg-blue-200 active:bg-blue-200 sm:flex dark:bg-gray-700 dark:hover:bg-blue-900 dark:active:bg-blue-900"
		style="left: calc({leftWidth}% - 4px); z-index:10;"
		on:mousedown={startDragVertical}
	>
		<div class="h-12 w-[0.05rem] rounded-full bg-gray-400"></div>
	</div>

	<div
		id="right-pane"
		class="relative flex h-full flex-col bg-gray-100"
		style="width: {100 - leftWidth}%"
	>
		{#key `${modelId}-${selectedTemplate?.name}`}
			<div class="w-full" style="height: {topHeight}%">
				<!-- Right top pane -->
				<JsonEditor
					bind:error
					bind:content={input}
					bind:selectedTemplate
					bind:selectedExampleInputId
					on:exampleChange={(e) => updateParams()}
				/>
			</div>
		{/key}

		<!-- svelte-ignore a11y_no_static_element_interactions -->
		<div
			class="hidden h-1 w-full cursor-row-resize items-center justify-center bg-gray-100 select-none hover:bg-blue-200 active:bg-blue-200 sm:flex dark:bg-gray-700 dark:hover:bg-blue-900 dark:active:bg-blue-900"
			style="top: calc({topHeight}% - 4px); z-index:10;"
			on:mousedown={startDragHorizontal}
		>
			<div class="h-[0.05rem] w-12 rounded-full bg-gray-400"></div>
		</div>

		<div class="w-full" style="height: {100 - topHeight}%">
			<!-- Right bottom pane -->
			<OutputViewer content={output} {error} />
		</div>
	</div>
</div>