- Conversation sidebar with create/delete/history - Chat area with streaming LLM responses (z-ai-web-dev-sdk) - Voice input via Web Speech API with recording indicator - Browser TTS auto-speak for assistant responses - Settings panel (voice, TTS, sidebar toggle) - Prisma schema: Conversation + Message models - API routes: /api/chat/stream, /api/conversations, /api/messages - Zustand store for state management - Web Speech API type declarations
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import ZAI from "z-ai-web-dev-sdk";
|
|
|
|
const SYSTEM_PROMPT = `You are Echo, a concise, helpful voice assistant. Follow these rules strictly:
|
|
|
|
1. **Verbal response**: Reply in ≤ 2 short sentences so it sounds natural when spoken aloud. Be direct and conversational.
|
|
2. **Local commands** (optional): If the user's request can be fulfilled by a local OS action, include a single JSON block at the very end of your response using this exact format:
|
|
|
|
\`\`\`json
|
|
{"action": "<command_name>", "params": {"key": "value"}}
|
|
\`\`\`
|
|
|
|
3. Do NOT include the JSON block in your spoken text.
|
|
4. Never use markdown formatting, bullet points, or headers in the spoken text.
|
|
5. If the user asks you to do something you cannot do, be honest about it briefly.`;
|
|
|
|
// Store conversation history per session
|
|
const sessionHistories = new Map<string, Array<{ role: string; content: string }>>();
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const { message, conversationId } = await request.json();
|
|
|
|
if (!message) {
|
|
return new Response(JSON.stringify({ error: "Message is required" }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
// Get or create session history
|
|
const history = sessionHistories.get(conversationId) || [];
|
|
history.push({ role: "user", content: message });
|
|
|
|
// Keep last 20 messages for context
|
|
const recentHistory = history.slice(-20);
|
|
|
|
const zai = await ZAI.create();
|
|
const completion = await zai.chat.completions.create({
|
|
messages: [
|
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
...recentHistory,
|
|
],
|
|
temperature: 0.7,
|
|
max_tokens: 300,
|
|
});
|
|
|
|
const assistantMessage =
|
|
completion.choices[0]?.message?.content || "I'm sorry, I couldn't process that.";
|
|
|
|
// Clean any thinking tags
|
|
const cleanedMessage = assistantMessage
|
|
.replace(/<think[^>]*>.*?<\/think\s*>/gs, "")
|
|
.replace(/```json.*?```/gs, "")
|
|
.trim();
|
|
|
|
// Store in history
|
|
history.push({ role: "assistant", content: cleanedMessage });
|
|
sessionHistories.set(conversationId, history);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
message: cleanedMessage,
|
|
rawMessage: assistantMessage,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error("Chat API error:", error);
|
|
return new Response(
|
|
JSON.stringify({
|
|
message: "Sorry, I had trouble thinking about that. Please try again.",
|
|
error: true,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
}
|