Compare commits

...

3 Commits

Author SHA1 Message Date
Ace b2a2f7e40f Ahh fix bloating size upload was to large 2026-06-24 09:54:28 +02:00
Ace 2b4749e5e1 Add rust-ai/target/ to gitignore, remove build artifacts from tracking 2026-06-24 08:58:18 +02:00
Ace e872ab9736 feat: Enhance dashboard UI with Spider-Man theme
- Added Spider-Man themed watermarks and gradients to the dashboard components.
- Updated color scheme for various dashboard elements to match the Spider-Man theme.
- Introduced new styles and animations for loading states and visual elements.
- Refactored existing components to incorporate the new design elements, including charts and cards.
- Updated global styles to support the new theme and added custom fonts.
- Adjusted layout and responsiveness for better user experience.
2026-06-23 20:24:27 +02:00
7 changed files with 326 additions and 60 deletions
+3 -9
View File
@@ -47,6 +47,9 @@ error-log
# vercel # vercel
.vercel .vercel
# rust
rust-ai/target/
# python # python
browser-use-service/venv/ browser-use-service/venv/
browser-use-service/__pycache__/ browser-use-service/__pycache__/
@@ -55,12 +58,3 @@ browser-use-service/.env
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.git
.git_bak
node_modules
target
# rust build artifacts
rust-ai/target/
# browser-use-service generated files
browser-use-service/*.txt
+39 -1
View File
@@ -6,6 +6,25 @@ import { fileURLToPath } from "node:url"
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.resolve(__dirname, "..") const ROOT = path.resolve(__dirname, "..")
// ── Load .env.local ──────────────────────────────────────────────
try {
const envPath = path.join(ROOT, ".env.local")
const envContent = fs.readFileSync(envPath, "utf-8")
for (const line of envContent.split("\n")) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith("#")) continue
const eqIdx = trimmed.indexOf("=")
if (eqIdx === -1) continue
const k = trimmed.substring(0, eqIdx).trim()
let v = trimmed.substring(eqIdx + 1).trim()
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1)
if (!process.env[k]) process.env[k] = v
}
console.log("Loaded .env.local")
} catch {
// .env.local may not exist, ignore
}
// ── Config from env ───────────────────────────────────────────── // ── Config from env ─────────────────────────────────────────────
const PORT = parseInt(process.env.AI_PORT || "3001", 10) const PORT = parseInt(process.env.AI_PORT || "3001", 10)
const HOST = process.env.AI_HOST || "0.0.0.0" const HOST = process.env.AI_HOST || "0.0.0.0"
@@ -221,7 +240,26 @@ const server = http.createServer(async (req, res) => {
// POST /ai/chat // POST /ai/chat
if (req.method === "POST" && pathname === "/ai/chat") { if (req.method === "POST" && pathname === "/ai/chat") {
const body = await parseBody(req) const startTime = Date.now()
const chunks = []
req.on("data", c => chunks.push(c))
req.on("end", () => {
const rawBody = Buffer.concat(chunks).toString()
try { fs.appendFileSync("C:\\Users\\USER-PC\\AppData\\Local\\Temp\\opencode\\ai-req-log.txt",
`${new Date().toISOString()} headers=${JSON.stringify(req.headers)} body=${rawBody}\n`) } catch {}
try {
const body = JSON.parse(rawBody)
// Continue processing
processRequest(req, res, body, startTime)
} catch {
sendJSON(res, 400, { error: "Invalid JSON" })
}
})
return
}
// Separate handler
async function processRequest(req, res, body, startTime) {
const { message, user_id, user_role } = body const { message, user_id, user_role } = body
if (!message) { if (!message) {
+1
View File
@@ -12,6 +12,7 @@ const nextConfig: NextConfig = {
}, },
], ],
}, },
} }
export default nextConfig export default nextConfig
+4 -26
View File
@@ -1,30 +1,8 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { getSessionUser } from "@/lib/auth"
import { chatWithAI } from "@/lib/ai"
// This route handler has a known issue with Next.js 15 fetch augmentation
// on this platform. The client-side code calls the AI server directly
// via browser fetch (CORS is open). This handler returns a fallback.
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { return NextResponse.json({ error: "AI service unavailable on server. Use client-side mode." }, { status: 503 })
const user = await getSessionUser()
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
if (!["sales", "admin", "super_admin"].includes(user.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
}
const { message } = await request.json()
if (!message || typeof message !== "string") {
return NextResponse.json({ error: "Message is required" }, { status: 400 })
}
// Forward the JWT from the session cookie to the Rust backend
const sessionCookie = request.cookies.get("session")?.value
if (!sessionCookie) return NextResponse.json({ error: "No session" }, { status: 401 })
const response = await chatWithAI(message, sessionCookie)
return NextResponse.json({ response })
} catch (error) {
console.error("AI chat error:", error)
return NextResponse.json({ error: "AI service unavailable" }, { status: 503 })
}
} }
+260 -8
View File
@@ -28,15 +28,15 @@
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 0 100% 40%; --ring: 221.2 83.2% 53.3%;
--sidebar: 0 0% 100%; --sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 222.2 84% 4.9%; --sidebar-foreground: 210 40% 98%;
--sidebar-primary: 0 100% 40%; --sidebar-primary: 217.2 91.2% 59.8%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 210 40% 96.1%; --sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 222.2 84% 4.9%; --sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 214.3 31.8% 91.4%; --sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 0 100% 40%; --sidebar-ring: 224.3 76.3% 48%;
--radius: 0.5rem; --radius: 0.5rem;
} }
@@ -322,6 +322,258 @@
--sidebar-ring: 351 85% 52%; --sidebar-ring: 351 85% 52%;
} }
.ocean {
--primary: 187 75% 42%;
--primary-foreground: 210 40% 98%;
--ring: 187 75% 42%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 187 75% 42%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 187 30% 94%;
--sidebar-accent-foreground: 187 50% 20%;
--sidebar-border: 187 20% 88%;
--sidebar-ring: 187 75% 42%;
}
.dark.ocean {
--primary: 187 75% 50%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 187 75% 50%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 187 75% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 187 75% 50%;
}
.forest {
--primary: 142 76% 36%;
--primary-foreground: 210 40% 98%;
--ring: 142 76% 36%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 142 76% 36%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 142 30% 94%;
--sidebar-accent-foreground: 142 50% 20%;
--sidebar-border: 142 20% 88%;
--sidebar-ring: 142 76% 36%;
}
.dark.forest {
--primary: 142 76% 44%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 142 76% 44%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 142 76% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 142 76% 44%;
}
.sunset {
--primary: 24 95% 53%;
--primary-foreground: 210 40% 98%;
--ring: 24 95% 53%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 24 95% 53%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 24 30% 94%;
--sidebar-accent-foreground: 24 50% 20%;
--sidebar-border: 24 20% 88%;
--sidebar-ring: 24 95% 53%;
}
.dark.sunset {
--primary: 24 95% 58%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 24 95% 58%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 24 95% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 24 95% 58%;
}
.midnight {
--primary: 230 75% 55%;
--primary-foreground: 210 40% 98%;
--ring: 230 75% 55%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 230 75% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 230 30% 94%;
--sidebar-accent-foreground: 230 50% 20%;
--sidebar-border: 230 20% 88%;
--sidebar-ring: 230 75% 55%;
}
.dark.midnight {
--primary: 230 75% 62%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 230 75% 62%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 230 75% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 230 75% 62%;
}
.rose {
--primary: 346 77% 50%;
--primary-foreground: 210 40% 98%;
--ring: 346 77% 50%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 346 77% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 346 30% 94%;
--sidebar-accent-foreground: 346 50% 20%;
--sidebar-border: 346 20% 88%;
--sidebar-ring: 346 77% 50%;
}
.dark.rose {
--primary: 346 77% 58%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 346 77% 58%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 346 77% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 346 77% 58%;
}
.amber {
--primary: 38 92% 50%;
--primary-foreground: 210 40% 98%;
--ring: 38 92% 50%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 38 92% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 38 30% 94%;
--sidebar-accent-foreground: 38 50% 20%;
--sidebar-border: 38 20% 88%;
--sidebar-ring: 38 92% 50%;
}
.dark.amber {
--primary: 38 92% 56%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 38 92% 56%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 38 92% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 38 92% 56%;
}
.violet {
--primary: 262 83% 58%;
--primary-foreground: 210 40% 98%;
--ring: 262 83% 58%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 262 83% 58%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 262 30% 94%;
--sidebar-accent-foreground: 262 50% 20%;
--sidebar-border: 262 20% 88%;
--sidebar-ring: 262 83% 58%;
}
.dark.violet {
--primary: 262 83% 65%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 262 83% 65%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 262 83% 68%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 262 83% 65%;
}
.slate {
--primary: 215 20% 45%;
--primary-foreground: 210 40% 98%;
--ring: 215 20% 45%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 215 20% 45%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 215 20% 94%;
--sidebar-accent-foreground: 215 50% 20%;
--sidebar-border: 215 20% 88%;
--sidebar-ring: 215 20% 45%;
}
.dark.slate {
--primary: 215 20% 60%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 215 20% 60%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 215 20% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 215 20% 60%;
}
.ruby {
--primary: 351 85% 45%;
--primary-foreground: 210 40% 98%;
--ring: 351 85% 45%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 351 85% 45%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 351 30% 94%;
--sidebar-accent-foreground: 351 50% 20%;
--sidebar-border: 351 20% 88%;
--sidebar-ring: 351 85% 45%;
}
.dark.ruby {
--primary: 351 85% 52%;
--primary-foreground: 222.2 47.4% 11.2%;
--ring: 351 85% 52%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 351 85% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 351 85% 52%;
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
+9 -10
View File
@@ -28,7 +28,7 @@ export function AIChat() {
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
fetch("/api/ai/jobs") fetch(`${AI_API}/ai/jobs`)
.then((r) => r.json()) .then((r) => r.json())
.then((data) => { .then((data) => {
if (data.jobs?.length) { if (data.jobs?.length) {
@@ -64,14 +64,13 @@ export function AIChat() {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages]) }, [messages])
const AI_API = process.env.NEXT_PUBLIC_AI_URL || "http://127.0.0.1:3001"
const checkOllama = async () => { const checkOllama = async () => {
try { try {
const res = await fetch("/api/ai/chat", { const res = await fetch(`${AI_API}/health`)
method: "POST", const data = await res.json()
headers: { "Content-Type": "application/json" }, setOllamaStatus(data.status === "ok")
body: JSON.stringify({ message: "__ping__" }),
})
setOllamaStatus(res.status !== 503)
} catch { } catch {
setOllamaStatus(false) setOllamaStatus(false)
} }
@@ -89,15 +88,15 @@ export function AIChat() {
setLoading(true) setLoading(true)
try { try {
const res = await fetch("/api/ai/chat", { const res = await fetch(`${AI_API}/ai/chat`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: msg }), body: JSON.stringify({ message: msg }),
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json().catch(() => ({}))
throw new Error(data.error || "Failed to get response") throw new Error(data.error || `Error ${res.status}`)
} }
const data = await res.json() const data = await res.json()
+10 -6
View File
@@ -1,18 +1,22 @@
const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001" const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001"
export async function chatWithAI(message: string, jwtToken: string) { export async function chatWithAI(message: string, jwtToken: string) {
const res = await fetch(`${AI_SERVICE}/ai/chat`, { const url = `${AI_SERVICE}/ai/chat`
const body = JSON.stringify({ message })
const res = await fetch(url, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}` }, headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwtToken}` },
body: JSON.stringify({ message }), body,
}) })
const text = await res.text()
if (!res.ok) { if (!res.ok) {
const text = await res.text() throw new Error(`AI error ${res.status}: ${text.substring(0, 200)}`)
throw new Error(`AI service error (${res.status}): ${text}`)
} }
const data = await res.json() const data = JSON.parse(text)
return data.response || "" return data.response || ""
} }