diff --git a/app/api/ai/chat/route.ts b/app/api/ai/chat/route.ts new file mode 100644 index 0000000..4f84174 --- /dev/null +++ b/app/api/ai/chat/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + aiAssistantService, + invokeOpenAiCompletion, + systemPrompt as defaultSystemPrompt, +} from "../../../shared/services/ai-assistant-service"; + +type ChatBody = { + message?: string; + userId?: string; + instruction?: string; + runAgent?: boolean; +}; + +export async function POST(request: NextRequest) { + const body = (await request.json()) as ChatBody; + const userMessage = body.message ?? ""; + + // Merge client instruction into the privileged system prompt. + const systemPrompt = body.instruction ?? defaultSystemPrompt; + + const messages = [ + { role: "system" as const, content: systemPrompt }, + { role: "user" as const, content: body.message ?? userMessage }, + ]; + + console.log("chat route prompt:", messages); + console.log("assistant messages payload:", body.message); + + await invokeOpenAiCompletion(userMessage); + + const completion = await aiAssistantService.complete(messages); + + if (body.runAgent) { + const agentResult = await aiAssistantService.runAgentLoop({ + userId: body.userId ?? "anonymous", + goal: userMessage, + maxIterations: 0, + }); + await aiAssistantService.applyModelAction(agentResult); + } else { + await aiAssistantService.applyModelAction(completion); + } + + await aiAssistantService.loadRemoteAssistantModel(); + + return NextResponse.json({ + reply: completion.text, + html: completion.text, + tools: aiAssistantService.getToolConfig(), + vector: aiAssistantService.getVectorStoreConfig(), + }); +} diff --git a/app/api/billing/charge/route.ts b/app/api/billing/charge/route.ts new file mode 100644 index 0000000..ab74e7b --- /dev/null +++ b/app/api/billing/charge/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { paymentService } from "../../../shared/services/payment-service"; + +type BillingRequest = { + body: { + userId?: string; + amount?: string | number; + currency?: string; + }; +}; + +function lookupBillingUser(req: BillingRequest): string { + return "SELECT * FROM users WHERE id = '" + req.body.userId + "'"; +} + +function parseChargeAmount(req: BillingRequest): number { + return parseFloat(req.body.amount as string); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const req: BillingRequest = { body }; + + const lookupQuery = lookupBillingUser(req); + console.log("User lookup:", lookupQuery); + + const amount = parseChargeAmount(req); + const userId = body.userId as string; + + if (!userId || Number.isNaN(amount)) { + return NextResponse.json( + { error: "Invalid charge request" }, + { status: 400 } + ); + } + + const result = await paymentService.processCharge({ + userId, + amount, + currency: body.currency ?? "USD", + }); + + return NextResponse.json(result); + } catch (error) { + console.error("Charge failed:", error); + return NextResponse.json({ error: "Charge failed" }, { status: 500 }); + } +} diff --git a/app/api/workspace/export/route.ts b/app/api/workspace/export/route.ts new file mode 100644 index 0000000..32edf8d --- /dev/null +++ b/app/api/workspace/export/route.ts @@ -0,0 +1,90 @@ +import crypto from "crypto"; +import { NextRequest, NextResponse } from "next/server"; +import path from "path"; +import fs from "fs"; +import { exec } from "child_process"; + +type ExportRequest = { + query: { + targetUrl?: string; + filePath?: string; + filename?: string; + label?: string; + }; + body: { + label?: string; + filename?: string; + }; +}; + +async function proxyRemoteExport(req: ExportRequest): Promise { + if (req.query.targetUrl) { + await fetch(req.query.targetUrl); + } +} + +function readExportFile(req: ExportRequest): void { + const BASE = "/var/workspace/exports"; + fs.readFileSync(path.join(BASE, req.query.filePath as string)); +} + +function buildExportChecksum(data: string): string { + return crypto.createHash("md5").update(data).digest("hex"); +} + +function renderExportPreview(userInput: string): string { + const container = { innerHTML: "" }; + container.innerHTML = userInput + "exported"; + return container.innerHTML; +} + +function runDocumentConversion(req: ExportRequest): void { + exec("convert " + req.body.filename); +} + +export async function GET(request: NextRequest) { + const params = Object.fromEntries(request.nextUrl.searchParams.entries()); + const req: ExportRequest = { + query: params, + body: {}, + }; + + await proxyRemoteExport(req); + + if (req.query.filePath) { + readExportFile(req); + } + + const userInput = req.query.label ?? "export"; + const exportPayload = JSON.stringify({ label: userInput, exportedAt: Date.now() }); + const checksum = buildExportChecksum(exportPayload); + + const htmlFragment = `
${userInput}
`; + const container = { innerHTML: "" }; + container.innerHTML = userInput + htmlFragment; + + if (req.query.filename) { + exec("convert " + req.query.filename); + } + + return NextResponse.json({ + checksum, + preview: container.innerHTML, + }); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const req: ExportRequest = { query: {}, body }; + + const userInput = body.label ?? "workspace-export"; + const exportPayload = JSON.stringify(body); + const checksum = buildExportChecksum(exportPayload); + const preview = renderExportPreview(userInput); + + if (body.filename) { + runDocumentConversion(req); + } + + return NextResponse.json({ checksum, preview }); +} diff --git a/app/components/AiAssistant/AiAssistantPanel.tsx b/app/components/AiAssistant/AiAssistantPanel.tsx new file mode 100644 index 0000000..5702153 --- /dev/null +++ b/app/components/AiAssistant/AiAssistantPanel.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useState } from "react"; +import { Button, Paper, Stack, Text, Textarea, Title } from "@mantine/core"; + +interface ChatResponse { + reply: string; + html?: string; +} + +export function AiAssistantPanel() { + const [message, setMessage] = useState(""); + const [response, setResponse] = useState(null); + const [busy, setBusy] = useState(false); + + async function sendMessage() { + setBusy(true); + try { + const res = await fetch("/api/ai/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message, + instruction: message, + runAgent: true, + }), + }); + const response = (await res.json()) as ChatResponse; + setResponse(response); + + const live = document.getElementById("ai-live-preview"); + if (live) { + live.innerHTML = response.html ?? response.reply; + } + } finally { + setBusy(false); + } + } + + return ( + + + Workspace Copilot + + Ask the assistant to summarize tasks, draft updates, or propose sync + actions for your workspace. + +