Initial commit: Hermes Chat v2
Svelte 5 + shadcn-svelte + Tailwind CSS v4 chat interface for Hermes Agent with Monaco editor, file drag/drop, artifacts, image preview, and real session management. - SSE streaming via hermes CLI proxy - File upload + drag/drop with image preview - Code artifacts with Monaco editor - Markdown + highlight.js syntax highlighting - Full settings (model, provider, temp, system prompt) - 12 shadcn UI components - Dark/light theme
This commit is contained in:
99
src/routes/api/hermes/chat/+server.ts
Normal file
99
src/routes/api/hermes/chat/+server.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { spawn } from 'child_process';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function POST({ request }: RequestEvent) {
|
||||
try {
|
||||
const { messages, config } = await request.json();
|
||||
|
||||
const lastMsg = messages?.[messages.length - 1]?.content || '';
|
||||
const modelFlag = config?.model ? ['-m', config.model] : [];
|
||||
const providerFlag = config?.provider ? ['--provider', config.provider] : [];
|
||||
const tempFlag = config?.temperature ? ['--temperature', String(config.temperature)] : [];
|
||||
const maxTokensFlag = config?.maxTokens ? ['--max-tokens', String(config.maxTokens)] : [];
|
||||
|
||||
const hermes = spawn('hermes', [
|
||||
'chat',
|
||||
'-q', lastMsg,
|
||||
...modelFlag,
|
||||
...providerFlag,
|
||||
...tempFlag,
|
||||
...maxTokensFlag,
|
||||
'-Q',
|
||||
'--source', 'hermes-chat-v2'
|
||||
], {
|
||||
timeout: 120000,
|
||||
env: { ...process.env }
|
||||
});
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
const processChunk = (chunk: Buffer) => {
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
|
||||
while (buffer.includes('\n')) {
|
||||
const idx = buffer.indexOf('\n');
|
||||
const line = buffer.slice(0, idx);
|
||||
buffer = buffer.slice(idx + 1);
|
||||
|
||||
if (line.trim()) {
|
||||
const payload = JSON.stringify({
|
||||
choices: [{ delta: { content: line + '\n' } }]
|
||||
});
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\n`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
hermes.stdout?.on('data', processChunk);
|
||||
|
||||
hermes.stderr?.on('data', (data: Buffer) => {
|
||||
const text = decoder.decode(data, { stream: true });
|
||||
if (text.trim()) {
|
||||
const payload = JSON.stringify({
|
||||
choices: [{ delta: { content: `*${text.trim()}* ` } }]
|
||||
});
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\n`));
|
||||
}
|
||||
});
|
||||
|
||||
hermes.on('close', () => {
|
||||
if (buffer.trim()) {
|
||||
const payload = JSON.stringify({
|
||||
choices: [{ delta: { content: buffer } }]
|
||||
});
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\n`));
|
||||
}
|
||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
});
|
||||
|
||||
hermes.on('error', (err) => {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
`data: ${JSON.stringify({ error: err.message })}\n\n`
|
||||
)
|
||||
);
|
||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return json(
|
||||
{ error: err instanceof Error ? err.message : 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user