Release 0.2.0

Co-authored-by: xinchengxx <xinchengxx@deepseek.com>
Co-authored-by: zhu-he <hezhu@deepseek.com>
This commit is contained in:
committed 2026-10-20 00:21:22 +08:00
1 parent 8cadfede70
commit 93b0fd229c
111 files changed
+15543 -4797

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "encoding-decoding-demo"
version = "0.1.0"
version.workspace = true
edition.workspace = true
description = "A local web interface for inspecting DeepSeek prompts and decoding model output."
description = "A local web interface for inspecting DeepSeek prompts and parsing model output."
publish = false
[dependencies]
+51 -41
View File
@@ -1,8 +1,9 @@
# Encoding & Decoding Demo
A web interface for rendering DeepSeek V4.1 prompts, inspecting special tokens,
and decoding complete model output into Chat Completions, Responses, and Messages
API formats.
A web interface for rendering DeepSeek V4 and V4.1 prompts, inspecting special
tokens, and parsing complete model output into Chat Completions, Responses, and
Messages responses. The demo renders prompts and parses model output; it does
not encode or decode token IDs.
## Usage
@@ -14,54 +15,61 @@ repository root:
cargo run -p encoding-decoding-demo --locked
```
Open [http://127.0.0.1:7778](http://127.0.0.1:7778). Set `DEMO_ADDR` to change the
listening address.
Open [http://127.0.0.1:7778](http://127.0.0.1:7778). Set `DEMO_ADDR` to change
the listening address.
Use the Encode / Decode buttons to switch between modes; only the selected
mode's editor and result are shown. In Encode mode, select an API format, edit
its request JSON, and render the request to inspect the model prompt. In Decode
mode, edit the complete assistant output and decode it. Model output is
rendered with the same special-token highlights as the prompt. Switching modes or
formats keeps each request draft and reuses the same model output.
Use the Render / Parse buttons to switch between modes; only the selected
mode's editor and result are shown. In Render mode, select an API format, edit
its request JSON, then render it and inspect the prompt. Render mode shows
prompt text; it does not encode the prompt into token IDs. In Parse mode, edit
the complete assistant output and parse it into the selected format's protocol
response. Model output is displayed with the same special-token highlights as
the prompt. Switching modes or formats keeps each request draft and reuses the
same model output.
The request's `model` name selects the prompt encoding. `DeepSeek-V4-Pro` and
`deepseek-v4-pro` render V4 prompts; `DeepSeek-V4.1-Flash`, `deepseek-flash`,
and `encoding-decoding-demo` render V4.1 prompts. A request without a `model`
uses `encoding-decoding-demo`. Any other name is rejected with HTTP 400.
The example output includes `<think>…</think>` reasoning, answer text, two DSML
weather tool calls (Beijing and Shanghai, each with string and numeric
arguments), and the end-of-turn marker `<|end▁of▁sentence|>`. Each protocol's
sample request declares the weather tool and includes the preceding tool
result and follow-up question.
arguments), and the end-of-sentence token (EOS) `<|end▁of▁sentence|>`. Each
protocol's sample request declares the weather tool and includes the preceding
tool result and follow-up question.
The decoded JSON view shows only response content, reasoning, and tool calls.
It omits finish status and server-generated metadata such as response
and tool-call IDs, model names, timestamps, token usage, and thinking
The protocol response view shows only answer content, reasoning content, and
tool calls. It omits finish status and server-generated metadata such as
response and tool-call IDs, model names, timestamps, token usage, and thinking
signatures. Tool inputs and arguments remain intact, including payload fields
with those names. The HTTP endpoint still returns the complete protocol
with those names. The HTTP endpoint responds with the complete protocol
response.
The decoder accepts complete assistant content, including the opening
`<think>` when reasoning is present, and an optional leading `<|Assistant|>`.
It consumes these frame markers before passing the content to the library's
stream parser. Output without an opening `<think>` is answer content; a
leading `</think>` from a non-thinking assistant prefix is also accepted.
Parse mode accepts complete assistant output, including the opening `<think>`
when reasoning is present, and an optional leading `<|Assistant|>`. It strips
these leading tokens before passing the content to the library's stream parser.
Output without an opening `<think>` is answer content; a leading `</think>`,
which the prompt ends with outside thinking mode, is also accepted.
Parsing starts outside tool-call markup because the complete output includes
its DSML opening block. Tool declarations, JSON output mode, and stop sequences
come from the selected protocol request.
Edit the model output, then use the adjacent decode button or Cmd/Ctrl+Enter. The UI
uses the default `stop` finish reason without displaying protocol status. If
the pasted output contains `<|end▁of▁sentence|>`, the demo treats its first
occurrence as EOS and discards that marker and everything after it.
Edit the model output, then use the adjacent Parse button or Cmd/Ctrl+Enter.
The UI uses the default `stop` finish reason without displaying protocol status.
If the pasted output contains the EOS, the demo treats its first occurrence as
the end of the output and discards that EOS and everything after it.
## HTTP endpoints
`POST /api/render` accepts `{ "format": "…", "body": { … } }` and returns the
prompt with highlighted segments.
`POST /api/render` accepts `{ "format": "…", "body": { … } }` and responds with
the rendered prompt and its highlighted segments.
`POST /api/decode` accepts the same request plus `output` and an optional
`POST /api/parse` accepts the same request plus `output` and an optional
`finish_reason` (default: `stop`). The formats are `chat_completions`,
`responses`, and `messages`. For example:
```sh
curl http://127.0.0.1:7778/api/decode \
curl http://127.0.0.1:7778/api/parse \
-H 'content-type: application/json' \
-d '{
"format": "chat_completions",
@@ -72,16 +80,18 @@ curl http://127.0.0.1:7778/api/decode \
```
The result is `{ "response": { … }, "segments": [ … ] }`. `segments` preserves
the complete supplied model output for highlighting, including its frame
markers. `response` is the complete protocol response, even when the input
request sets `stream: true`. The demo
uses the supplied model name or `encoding-decoding-demo`, a demonstration response ID,
and zero token usage because no tokenizer or backend usage data is supplied.
Decoding uses the library's stream parser and protocol response accumulators.
API callers can supply `length` or `content_filter` as the backend finish
reason. EOS uses `stop` instead, and matched request stop sequences take
the complete supplied model output for highlighting, including the leading
`<|Assistant|>` and `<think>`. `response` is the complete protocol response,
even when the input request sets `stream: true`. The demo uses the supplied
model name or `encoding-decoding-demo`, a demonstration response ID, and zero
token usage because no tokenizer or backend usage data is supplied.
The endpoint parses the output with the library's `StreamProcessor` and
accumulates the protocol chunks into the complete response. API callers can
supply `length` or `content_filter` as the backend finish reason. Output that
contains the EOS uses `stop` instead, and matched request stop sequences take
precedence.
This example has no authentication. Keep the default loopback address for local
use. It renders prompts and decodes supplied text; it does not execute
inference, run tools, or fetch image URLs.
use. It renders prompts and parses model output; it does not encode
conversations into token IDs, decode token IDs, execute inference, run tools, or
fetch image URLs.
+82 -68
View File
@@ -11,21 +11,16 @@ use deepseek_recipe::anthropic::MessagesRequest;
use deepseek_recipe::error_response::ErrorResponse;
use deepseek_recipe::openai::{ChatCompletionRequest, ResponsesRequest};
use deepseek_recipe::request::{
ConversationRequest, ConversionError, ConversionOptions, ProtocolRequest,
ConversationRequest, ConversionError, ConversionOptions, ModelVersion, ProtocolRequest,
};
use deepseek_recipe::response::ProtocolResponse;
use deepseek_recipe::stream::state_machine::ReasoningStage;
use deepseek_recipe::response::{Accumulate, ProtocolResponse};
use deepseek_recipe::stream::{
ChunkGenerator, InferenceChunk, InferenceFinishReason, StreamProcessor,
ChunkGenerator, InferenceChunk, InferenceFinishReason, StreamProcessor, ThinkingInitialStage,
};
use deepseek_recipe::util::append_delta::AppendDelta;
use deepseek_recipe_core::multimodal::IMAGE_SPECIAL_TOKEN;
use deepseek_recipe_core::special_tokens::SpecialToken;
use deepseek_recipe_encoding::PromptEncoding;
use deepseek_recipe_encoding::v4::dsv4::DeepseekV4Encoding;
use deepseek_recipe_encoding::v4::dsv41::DeepseekV41Encoding;
use deepseek_recipe_encoding::v4::{
ASSISTANT_SP_TOKEN, BOS_TOKEN, DSML_SP_TOKEN, EOS_TOKEN, LATEST_REMINDER_SP_TOKEN,
SYSTEM_SP_TOKEN, THINKING_END_TOKEN, THINKING_START_TOKEN, USER_SP_TOKEN,
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tokio_stream::StreamExt;
@@ -67,47 +62,47 @@ impl Highlight {
const HIGHLIGHTS: &[Highlight] = &[
Highlight::new(
"bos",
&[BOS_TOKEN],
"Begin of sentence. Marks the start of the prompt.",
&[SpecialToken::Bos.default_str()],
"Beginning-of-sentence token (BOS). Marks the start of the prompt.",
),
Highlight::new(
"system",
&[SYSTEM_SP_TOKEN],
&[SpecialToken::System.default_str()],
"System turn marker. Starts a system message.",
),
Highlight::new(
"user",
&[USER_SP_TOKEN],
&[SpecialToken::User.default_str()],
"User turn marker. Starts a user message.",
),
Highlight::new(
"assistant",
&[ASSISTANT_SP_TOKEN],
&[SpecialToken::Assistant.default_str()],
"Assistant turn marker. Starts an assistant message.",
),
Highlight::new(
"thinking_start",
&[THINKING_START_TOKEN],
&[SpecialToken::ThinkingStart.default_str()],
"Starts the reasoning content.",
),
Highlight::new(
"thinking_end",
&[THINKING_END_TOKEN],
&[SpecialToken::ThinkingEnd.default_str()],
"Ends the reasoning content.",
),
Highlight::new(
"latest_reminder",
&[LATEST_REMINDER_SP_TOKEN],
&[SpecialToken::LatestReminder.default_str()],
"Starts the latest reminder message.",
),
Highlight::new(
"eos",
&[EOS_TOKEN],
"End of sentence. Terminates the message.",
&[SpecialToken::Eos.default_str()],
"End-of-sentence token (EOS). Ends an assistant message.",
),
Highlight::new(
"dsml",
&[DSML_SP_TOKEN],
&[SpecialToken::Dsml.default_str()],
"DeepSeek markup tag. Structures tool calls in the prompt.",
),
Highlight::new(
@@ -117,7 +112,7 @@ const HIGHLIGHTS: &[Highlight] = &[
),
Highlight::new(
"image",
&[IMAGE_SPECIAL_TOKEN],
&[SpecialToken::Image.default_str()],
"Marks one image in the prompt.",
),
Highlight::new(
@@ -219,34 +214,34 @@ struct RenderResponse {
#[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum DecodeFinishReason {
enum ParseFinishReason {
#[default]
Stop,
Length,
ContentFilter,
}
impl From<DecodeFinishReason> for InferenceFinishReason {
fn from(reason: DecodeFinishReason) -> Self {
impl From<ParseFinishReason> for InferenceFinishReason {
fn from(reason: ParseFinishReason) -> Self {
match reason {
DecodeFinishReason::Stop => Self::Stop,
DecodeFinishReason::Length => Self::Length,
DecodeFinishReason::ContentFilter => Self::ContentFilter,
ParseFinishReason::Stop => Self::Stop,
ParseFinishReason::Length => Self::Length,
ParseFinishReason::ContentFilter => Self::ContentFilter,
}
}
}
#[derive(Debug, Deserialize)]
struct DecodeRequest {
struct ParseRequest {
format: ApiFormat,
body: serde_json::Value,
output: String,
#[serde(default)]
finish_reason: DecodeFinishReason,
finish_reason: ParseFinishReason,
}
#[derive(Debug, Serialize)]
struct DecodeResponse {
struct ParseResponse {
response: serde_json::Value,
segments: Vec<Segment>,
}
@@ -260,33 +255,50 @@ fn bad_request(detail: String) -> DemoError {
fn conversion_failed(error: ConversionError) -> DemoError {
let status =
StatusCode::from_u16(error.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status, Json(error.into_response()))
(status, Json(error.into_error_response()))
}
fn convert_request<T>(mut body: serde_json::Value) -> Result<ConversationRequest, DemoError>
/// Convert a protocol request, returning it alongside the requested model name
/// echoed into responses.
fn convert_request<T>(
mut body: serde_json::Value,
) -> Result<(ConversationRequest, String), DemoError>
where
T: ProtocolRequest + DeserializeOwned,
{
// Requests use V4.1 prompt rendering and a default model name.
// A request without a model name uses the default name, which targets V4.1.
const DEFAULT_MODEL: &str = "encoding-decoding-demo";
if let Some(body) = body.as_object_mut() {
body.entry("model")
.or_insert_with(|| "encoding-decoding-demo".into());
body.entry("model").or_insert_with(|| DEFAULT_MODEL.into());
}
let model = body["model"].as_str().unwrap_or(DEFAULT_MODEL).to_owned();
let request: T = serde_json::from_value(body)
.map_err(|err| bad_request(format!("invalid request body: {err}")))?;
request
.convert(ConversionOptions::default())
.map_err(conversion_failed)
// The demo also accepts the DeepSeek API model names.
let converted = request
.convert(
ConversionOptions::default()
.with_model_alias(DEFAULT_MODEL, ModelVersion::DeepseekV41Flash)
.with_model_alias("deepseek-v4-pro", ModelVersion::DeepseekV4Pro)
.with_model_alias("deepseek-flash", ModelVersion::DeepseekV41Flash),
)
.map_err(conversion_failed)?;
Ok((converted, model))
}
fn render_prompt<T>(body: serde_json::Value) -> Result<String, DemoError>
where
T: ProtocolRequest + DeserializeOwned,
{
let request = convert_request::<T>(body)?;
Ok(DeepseekV41Encoding::new()
.render_conversation(&request.conversation)
.prompt)
let (request, _) = convert_request::<T>(body)?;
// The model version of the request selects the prompt encoding.
let rendered = match request.model_version {
ModelVersion::DeepseekV4Pro => {
DeepseekV4Encoding::new().render_conversation(&request.conversation)
}
_ => DeepseekV41Encoding::new().render_conversation(&request.conversation),
};
Ok(rendered.prompt)
}
async fn render_handler(
@@ -301,36 +313,38 @@ async fn render_handler(
Ok(Json(RenderResponse { prompt, segments }))
}
async fn decode_output<T>(request: DecodeRequest) -> Result<serde_json::Value, DemoError>
async fn parse_output<T>(request: ParseRequest) -> Result<serde_json::Value, DemoError>
where
T: ProtocolRequest + DeserializeOwned,
<<T::Response as ProtocolResponse>::ChunkGenerator as ChunkGenerator>::Chunk: Send,
{
let mut converted = convert_request::<T>(request.body)?;
let (mut converted, model) = convert_request::<T>(request.body)?;
// This endpoint always accumulates a complete response, including usage,
// even when the input request originally selected streaming transport.
converted.stream = false;
let (output, finish_reason) = match request.output.split_once(EOS_TOKEN) {
let (output, finish_reason) = match request.output.split_once(SpecialToken::Eos.default_str()) {
Some((output, _)) => (output, InferenceFinishReason::Stop),
None => (request.output.as_str(), request.finish_reason.into()),
};
// The demo accepts complete assistant output. Consume its leading frame
// markers here; the stream parser normally starts after a prompt prefill.
let output = output.strip_prefix(ASSISTANT_SP_TOKEN).unwrap_or(output);
let (thinking, output) = match output.strip_prefix(THINKING_START_TOKEN) {
// The stream parser expects output that starts after the prompt's
// assistant-turn start (`<|Assistant|><think>` or `</think>`). The demo
// accepts complete assistant output, so it strips that start here.
let output = output
.strip_prefix(SpecialToken::Assistant.default_str())
.unwrap_or(output);
let (thinking, output) = match output.strip_prefix(SpecialToken::ThinkingStart.default_str()) {
Some(output) => (true, output),
None => (
false,
output.strip_prefix(THINKING_END_TOKEN).unwrap_or(output),
output
.strip_prefix(SpecialToken::ThinkingEnd.default_str())
.unwrap_or(output),
),
};
converted.conversation.thinking_mode = thinking;
converted.parsing_options.reasoning_initial_stage = thinking.then_some(ReasoningStage::Start);
converted.parsing_options.tool_call_initial_stage = false;
let model = converted
.model
.clone()
.unwrap_or_else(|| "encoding-decoding-demo".into());
converted.parsing_options.thinking_initial_stage =
thinking.then_some(ThinkingInitialStage::ReasoningStart);
converted.parsing_options.starts_in_tool_calls = false;
let id = "demo-id".to_owned();
let generator = T::chunk_generator(&converted, id.clone(), model.clone());
let processor = StreamProcessor::new(generator, converted.parsing_options);
@@ -338,7 +352,7 @@ where
InferenceChunk::Text {
content: output.to_owned(),
// Raw text alone does not supply tokenizer or backend usage data.
content_tokens: 0,
token_count: 0,
},
InferenceChunk::Finish { finish_reason },
]);
@@ -347,10 +361,10 @@ where
.map(|elapsed| elapsed.as_secs())
.unwrap_or(0);
let mut response = T::Response::new(id, model, created, 0, 0);
let mut chunks = pin!(processor.process(inference));
let mut chunks = pin!(processor.into_protocol_stream(inference));
while let Some(chunk) = chunks.next().await {
response
.append(chunk.map_err(|error| {
.accumulate(chunk.map_err(|error| {
conversion_failed(ConversionError::internal(error.to_string()))
})?);
}
@@ -358,16 +372,16 @@ where
.map_err(|error| conversion_failed(ConversionError::internal(error.to_string())))
}
async fn decode_handler(
Json(request): Json<DecodeRequest>,
) -> Result<Json<DecodeResponse>, DemoError> {
async fn parse_handler(
Json(request): Json<ParseRequest>,
) -> Result<Json<ParseResponse>, DemoError> {
let segments = segment(&request.output);
let response = match request.format {
ApiFormat::ChatCompletions => decode_output::<ChatCompletionRequest>(request).await?,
ApiFormat::Responses => decode_output::<ResponsesRequest>(request).await?,
ApiFormat::Messages => decode_output::<MessagesRequest>(request).await?,
ApiFormat::ChatCompletions => parse_output::<ChatCompletionRequest>(request).await?,
ApiFormat::Responses => parse_output::<ResponsesRequest>(request).await?,
ApiFormat::Messages => parse_output::<MessagesRequest>(request).await?,
};
Ok(Json(DecodeResponse { response, segments }))
Ok(Json(ParseResponse { response, segments }))
}
fn asset(content_type: &'static str, body: &'static str) -> impl IntoResponse {
@@ -409,7 +423,7 @@ async fn main() {
get(|| async { binary_asset("image/png", FAVICON_PNG) }),
)
.route("/api/render", post(render_handler))
.route("/api/decode", post(decode_handler));
.route("/api/parse", post(parse_handler));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("deepseek-recipe demo page: http://{addr}");
+42 -41
View File
@@ -24,6 +24,7 @@ const SAMPLES = {
tool_calls: [
{
id: "call_1",
type: "function",
function: {
name: "get_weather",
arguments: JSON.stringify({ location: "Beijing", days: 3 }),
@@ -79,7 +80,7 @@ const SAMPLES = {
};
const DEFAULT_FORMAT = "chat_completions";
const EOT = "<|end▁of▁sentence|>";
const EOS = "<|end▁of▁sentence|>";
const OUTPUT_SAMPLE = `<think>The user wants to compare the weather in Beijing and Shanghai for the next three days, so I need forecasts for both cities.
</think>
I'll check the weather in Beijing and Shanghai for the next three days.
@@ -93,7 +94,7 @@ I'll check the weather in Beijing and Shanghai for the next three days.
<|DSML| parameter name="location" string="true">Shanghai</|DSML| parameter>
<|DSML| parameter name="days" string="false">3</|DSML| parameter>
</|DSML| invoke>
</|DSML| calls>${EOT}`;
</|DSML| calls>${EOS}`;
const tabs = Array.from(document.querySelectorAll(".tab"));
const modeTabs = Array.from(document.querySelectorAll(".mode-tab"));
@@ -104,12 +105,12 @@ const errorBox = document.getElementById("error");
const tip = document.getElementById("tip");
const modelOutput = document.getElementById("model-output");
const modelOutputPreview = document.getElementById("model-output-preview");
const decodedOutput = document.getElementById("decoded-output");
const decodeMeta = document.getElementById("decode-meta");
const decodeError = document.getElementById("decode-error");
const parsedOutput = document.getElementById("parsed-output");
const parseMeta = document.getElementById("parse-meta");
const parseError = document.getElementById("parse-error");
const responseFormat = document.getElementById("response-format");
const state = { mode: "encode", format: DEFAULT_FORMAT, drafts: { ...SAMPLES }, renderId: 0, decodeId: 0 };
const state = { mode: "render", format: DEFAULT_FORMAT, drafts: { ...SAMPLES }, renderId: 0, parseId: 0 };
function pretty(value) {
return JSON.stringify(value, null, 2);
@@ -121,7 +122,7 @@ function pick(value, keys) {
// Select protocol fields explicitly: tool inputs and argument strings are
// payloads, so metadata-like keys inside them must remain untouched.
function decodedContent(format, response) {
function parsedContent(format, response) {
switch (format) {
case "chat_completions":
return {
@@ -157,8 +158,8 @@ function decodedContent(format, response) {
}
function runMode() {
if (state.mode === "encode") renderPrompt();
else decodeOutput();
if (state.mode === "render") renderPrompt();
else parseOutput();
}
function setMode(mode) {
@@ -169,7 +170,7 @@ function setMode(mode) {
tab.setAttribute("aria-selected", String(selected));
tab.tabIndex = selected ? 0 : -1;
}
for (const name of ["encode", "decode"]) {
for (const name of ["render", "parse"]) {
document.getElementById(`${name}-panel`).hidden = name !== mode;
document.getElementById(`${name}-actions`).hidden = name !== mode;
}
@@ -189,9 +190,9 @@ function setFormat(format) {
}
input.value = state.drafts[format];
state.renderId++;
state.decodeId++;
state.parseId++;
clearPrompt();
clearDecoded();
clearParsed();
hideError();
runMode();
}
@@ -243,30 +244,30 @@ async function post(url, body) {
return payload;
}
async function decodeOutput() {
const id = ++state.decodeId;
async function parseOutput() {
const id = ++state.parseId;
const format = state.format;
clearDecoded();
decodeMeta.textContent = "Decoding…";
decodedOutput.setAttribute("aria-busy", "true");
clearParsed();
parseMeta.textContent = "Parsing…";
parsedOutput.setAttribute("aria-busy", "true");
try {
const body = requestBody();
const payload = await post("/api/decode", {
const payload = await post("/api/parse", {
format,
body,
output: modelOutput.value,
});
if (id !== state.decodeId) return;
if (id !== state.parseId) return;
renderModelOutput(payload.segments);
decodedOutput.textContent = pretty(decodedContent(format, payload.response));
decodeMeta.textContent = "";
parsedOutput.textContent = pretty(parsedContent(format, payload.response));
parseMeta.textContent = "";
} catch (err) {
if (id !== state.decodeId) return;
clearDecoded();
decodeError.textContent = err.message;
decodeError.hidden = false;
if (id !== state.parseId) return;
clearParsed();
parseError.textContent = err.message;
parseError.hidden = false;
} finally {
if (id === state.decodeId) decodedOutput.setAttribute("aria-busy", "false");
if (id === state.parseId) parsedOutput.setAttribute("aria-busy", "false");
}
}
@@ -277,17 +278,17 @@ function clearPrompt() {
tip.hidden = true;
}
function clearDecoded() {
decodedOutput.textContent = "";
decodedOutput.setAttribute("aria-busy", "false");
decodeMeta.textContent = "";
decodeError.hidden = true;
function clearParsed() {
parsedOutput.textContent = "";
parsedOutput.setAttribute("aria-busy", "false");
parseMeta.textContent = "";
parseError.hidden = true;
}
function invalidateDecoded() {
state.decodeId++;
clearDecoded();
decodeMeta.textContent = "Input changed, ready to decode";
function invalidateParsed() {
state.parseId++;
clearParsed();
parseMeta.textContent = "Output changed; parse again";
}
function render(payload) {
@@ -296,7 +297,7 @@ function render(payload) {
fragment.append(renderSegment(segment));
}
output.replaceChildren(fragment);
meta.textContent = `${payload.prompt.length} chars`;
meta.textContent = `${payload.prompt.length} characters`;
}
function syncModelOutputScroll() {
@@ -396,20 +397,20 @@ output.addEventListener("mouseout", (event) => {
});
document.getElementById("render-prompt").addEventListener("click", renderPrompt);
document.getElementById("decode-output").addEventListener("click", decodeOutput);
document.getElementById("parse-output").addEventListener("click", parseOutput);
input.addEventListener("input", () => {
state.drafts[state.format] = input.value;
state.renderId++;
clearPrompt();
hideError();
meta.textContent = "Request changed, ready to render";
invalidateDecoded();
meta.textContent = "Request changed; render again";
invalidateParsed();
});
modelOutput.addEventListener("input", () => {
renderModelOutput();
invalidateDecoded();
invalidateParsed();
});
modelOutput.addEventListener("scroll", syncModelOutputScroll);
new ResizeObserver(syncModelOutputScroll).observe(modelOutput);
@@ -417,7 +418,7 @@ new ResizeObserver(syncModelOutputScroll).observe(modelOutput);
modelOutput.addEventListener("keydown", (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
event.preventDefault();
decodeOutput();
parseOutput();
}
});
+15 -15
View File
@@ -13,10 +13,10 @@
<span class="badge">Encoding &amp; Decoding Demo</span>
</header>
<nav class="toolbar" aria-label="Encoding, decoding, and API format">
<nav class="toolbar" aria-label="Mode and API format">
<div class="mode-tabs" role="tablist" aria-label="Mode">
<button id="mode-encode" class="mode-tab active" type="button" data-mode="encode" role="tab" aria-controls="encode-panel" aria-selected="true" tabindex="0">Encode</button>
<button id="mode-decode" class="mode-tab" type="button" data-mode="decode" role="tab" aria-controls="decode-panel" aria-selected="false" tabindex="-1">Decode</button>
<button id="mode-render" class="mode-tab active" type="button" data-mode="render" role="tab" aria-controls="render-panel" aria-selected="true" tabindex="0">Render</button>
<button id="mode-parse" class="mode-tab" type="button" data-mode="parse" role="tab" aria-controls="parse-panel" aria-selected="false" tabindex="-1">Parse</button>
</div>
<span class="toolbar-divider" aria-hidden="true"></span>
<div class="tabs" role="tablist" aria-label="API format">
@@ -27,39 +27,39 @@
</nav>
<main>
<section id="encode-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-encode">
<section id="render-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-render">
<div id="error" class="error" role="alert" hidden></div>
<div class="grid">
<section class="pane">
<header class="pane-header">
<div class="pane-heading">
<h2><label for="input">Original input</label></h2>
<h2><label for="input">Request JSON</label></h2>
</div>
<div id="encode-actions" class="actions">
<button id="render-prompt" class="primary" type="button" title="Encode (⌘/Ctrl + Enter)">Encode</button>
<div id="render-actions" class="actions">
<button id="render-prompt" class="primary" type="button" title="Render (⌘/Ctrl + Enter)">Render</button>
</div>
</header>
<textarea id="input" spellcheck="false" autocomplete="off"></textarea>
</section>
<section class="pane">
<header class="pane-header">
<h2>Encoded result <span id="meta" class="meta"></span></h2>
<h2>Rendered prompt <span id="meta" class="meta"></span></h2>
</header>
<pre id="output" class="prompt"></pre>
</section>
</div>
</section>
<section id="decode-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-decode" hidden>
<div id="decode-error" class="error" role="alert" hidden></div>
<section id="parse-panel" class="mode-panel" role="tabpanel" aria-labelledby="mode-parse" hidden>
<div id="parse-error" class="error" role="alert" hidden></div>
<div class="grid">
<section class="pane">
<header class="pane-header">
<div class="pane-heading">
<h2><label for="model-output">Original output</label></h2>
<h2><label for="model-output">Model output</label></h2>
</div>
<div id="decode-actions" class="actions" hidden>
<button id="decode-output" class="primary" type="button" title="Decode (⌘/Ctrl + Enter)">Decode</button>
<div id="parse-actions" class="actions" hidden>
<button id="parse-output" class="primary" type="button" title="Parse (⌘/Ctrl + Enter)">Parse</button>
</div>
</header>
<div class="model-editor">
@@ -69,9 +69,9 @@
</section>
<section class="pane">
<header class="pane-header">
<h2>Decoded result <span id="response-format" class="meta">Chat Completions</span> <span id="decode-meta" class="meta" role="status"></span></h2>
<h2>Protocol response <span id="response-format" class="meta">Chat Completions</span> <span id="parse-meta" class="meta" role="status"></span></h2>
</header>
<pre id="decoded-output" class="prompt response" aria-label="Decoded result"></pre>
<pre id="parsed-output" class="prompt response" aria-label="Protocol response"></pre>
</section>
</div>
</section>