20738f3623
Replace the legacy React UI with the new Svelte-based one. Introduce a Playground in the UI to quickly test out text, image, text to speech and speech to text models behind llama-swap.
Key Changes
New Svelte UI (ui-svelte/)
- Multi-tab Playground with Chat, Image Generation, Audio Transcription, and Speech interfaces
- Chat: message editing/regeneration, markdown rendering with LaTeX math support, image attachments, code syntax highlighting
- Image: size selector, download/fullscreen viewing
- Audio: transcription with peer support
- Speech: voice caching with manual refresh, download button
- Responsive mobile layout with collapsible navigation
- XSS fixes and accessibility improvements
Proxy Improvements
- Add gzip/brotli compression for UI static assets (proxy/ui_compress.go)
- Add GET /v1/audio/voices?model={model} endpoint for voice listing
- Add peer support for /v1/audio/transcriptions
109 lines
2.3 KiB
TypeScript
109 lines
2.3 KiB
TypeScript
import type { ChatMessage, ChatCompletionRequest } from "./types";
|
|
|
|
export interface StreamChunk {
|
|
content: string;
|
|
reasoning_content?: string;
|
|
done: boolean;
|
|
}
|
|
|
|
export interface ChatOptions {
|
|
temperature?: number;
|
|
}
|
|
|
|
function parseSSELine(line: string): StreamChunk | null {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || !trimmed.startsWith("data: ")) {
|
|
return null;
|
|
}
|
|
|
|
const data = trimmed.slice(6);
|
|
if (data === "[DONE]") {
|
|
return { content: "", done: true };
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
const delta = parsed.choices?.[0]?.delta;
|
|
const content = delta?.content || "";
|
|
const reasoning_content = delta?.reasoning_content || "";
|
|
|
|
if (content || reasoning_content) {
|
|
return { content, reasoning_content, done: false };
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function* streamChatCompletion(
|
|
model: string,
|
|
messages: ChatMessage[],
|
|
signal?: AbortSignal,
|
|
options?: ChatOptions
|
|
): AsyncGenerator<StreamChunk> {
|
|
const request: ChatCompletionRequest = {
|
|
model,
|
|
messages,
|
|
stream: true,
|
|
temperature: options?.temperature,
|
|
};
|
|
|
|
const response = await fetch("/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(request),
|
|
signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Chat API error: ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
throw new Error("Response body is not readable");
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
|
|
if (done) {
|
|
break;
|
|
}
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() || "";
|
|
|
|
for (const line of lines) {
|
|
const result = parseSSELine(line);
|
|
if (result?.done) {
|
|
yield result;
|
|
return;
|
|
}
|
|
if (result) {
|
|
yield result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process any remaining buffer
|
|
const result = parseSSELine(buffer);
|
|
if (result && !result.done) {
|
|
yield result;
|
|
}
|
|
|
|
yield { content: "", done: true };
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
}
|