From 9ef1027cc6a57322eec76a4c7136d7be0c1a7bc3 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Sat, 3 Jan 2026 18:18:53 -0700 Subject: [PATCH] Add permissions file, enhance HubertChat init & retry, update API routes - Introduce .claude/.fuse_hidden... with explicit allow permissions - Add fetchWithTimeout utility for robust network calls - Implement init error handling and retry mechanism in HubChat - Update API handlers for new visitor and chat routes - Adjust wrangler.jsonc for deployment changes Signed: Hubert The Eunuch --- .claude/.fuse_hidden009df63e00000355 | 16 ++ src/components/HubertChat.tsx | 223 ++++++++++++++++++++++++--- src/components/sections/Hubert.astro | 4 +- src/pages/api/hubert/chat.ts | 15 +- src/pages/api/hubert/new-visitor.ts | 30 +++- wrangler.jsonc | 6 +- 6 files changed, 257 insertions(+), 37 deletions(-) create mode 100644 .claude/.fuse_hidden009df63e00000355 diff --git a/.claude/.fuse_hidden009df63e00000355 b/.claude/.fuse_hidden009df63e00000355 new file mode 100644 index 0000000..9f0ed87 --- /dev/null +++ b/.claude/.fuse_hidden009df63e00000355 @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:openrouter.ai)", + "Bash(node:*)", + "Bash(curl:*)", + "Bash(pnpm build:*)", + "Bash(find:*)", + "Bash(pnpm add:*)", + "WebFetch(domain:substance.biohazardvfx.com)", + "WebFetch(domain:substrate.biohazardvfx.com)" + ], + "deny": [], + "ask": [] + } +} diff --git a/src/components/HubertChat.tsx b/src/components/HubertChat.tsx index acb3fe4..58d9328 100644 --- a/src/components/HubertChat.tsx +++ b/src/components/HubertChat.tsx @@ -1,5 +1,4 @@ import React, { useState, useRef, useEffect } from 'react'; -import { randomUUID } from 'crypto'; interface Message { role: 'user' | 'assistant' | 'system'; @@ -7,6 +6,24 @@ interface Message { timestamp: string; } +// Utility: Fetch with timeout +const fetchWithTimeout = async (url: string, options: RequestInit = {}, timeout = 8000) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + throw error; + } +}; + export default function HubertChat() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); @@ -14,18 +31,35 @@ export default function HubertChat() { const [conversationId, setConversationId] = useState(null); const [isTyping, setIsTyping] = useState(false); const [isInitializing, setIsInitializing] = useState(true); + const [initError, setInitError] = useState(null); const messagesEndRef = useRef(null); // Initialize visitor on mount useEffect(() => { const initVisitor = async () => { try { + console.log('[Hubert] Starting initialization...'); setIsInitializing(true); - const response = await fetch('/api/hubert/new-visitor', { method: 'POST' }); + setInitError(null); + + console.log('[Hubert] Fetching /api/hubert/new-visitor...'); + const response = await fetchWithTimeout('/api/hubert/new-visitor', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, 8000); // 8 second timeout + + console.log('[Hubert] Response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const data = await response.json(); + console.log('[Hubert] Initialization successful:', data); + setVisitorId(data.visitor_id); setConversationId(data.conversation_id); - + // Add system welcome message from Hubert setMessages([{ role: 'system', @@ -33,27 +67,128 @@ export default function HubertChat() { timestamp: new Date().toISOString(), }]); } catch (error) { - console.error('Failed to initialize Hubert:', error); + console.error('[Hubert] Initialization failed:', error); + + let errorMessage = '/// ERROR: UNKNOWN_FAILURE'; + + if (error instanceof Error) { + if (error.name === 'AbortError') { + errorMessage = '/// ERROR: TIMEOUT - API_UNRESPONSIVE'; + console.error('[Hubert] Request timed out after 8 seconds'); + } else if (error.message.includes('Failed to fetch')) { + errorMessage = '/// ERROR: NETWORK_FAILURE - CHECK_API_ROUTE'; + console.error('[Hubert] Network error - API route may not exist'); + } else { + errorMessage = `/// ERROR: ${error.message}`; + } + } + + setInitError(errorMessage); setMessages([{ role: 'system', - content: '/// ERROR: HUBERT_OFFLINE - REFRESH_PAGE', + content: errorMessage + '\\n\\nCLICK [RETRY] BELOW', timestamp: new Date().toISOString(), }]); } finally { + console.log('[Hubert] Initialization complete, setting isInitializing to false'); setIsInitializing(false); } }; initVisitor(); }, []); - // Auto-scroll to bottom + // Retry initialization + const retryInit = () => { + console.log('[Hubert] Retrying initialization...'); + setIsInitializing(true); + setInitError(null); + setMessages([]); + + // Re-trigger initialization + const initVisitor = async () => { + try { + console.log('[Hubert] Starting initialization...'); + setIsInitializing(true); + setInitError(null); + + console.log('[Hubert] Fetching /api/hubert/new-visitor...'); + const response = await fetchWithTimeout('/api/hubert/new-visitor', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, 8000); + + console.log('[Hubert] Response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + console.log('[Hubert] Initialization successful:', data); + + setVisitorId(data.visitor_id); + setConversationId(data.conversation_id); + + setMessages([{ + role: 'system', + content: `/// HUBERT_EUNUCH /// ONLINE\\n\\nI suppose you want something. State your business.`, + timestamp: new Date().toISOString(), + }]); + } catch (error) { + console.error('[Hubert] Initialization failed:', error); + + let errorMessage = '/// ERROR: UNKNOWN_FAILURE'; + + if (error instanceof Error) { + if (error.name === 'AbortError') { + errorMessage = '/// ERROR: TIMEOUT - API_UNRESPONSIVE'; + console.error('[Hubert] Request timed out after 8 seconds'); + } else if (error.message.includes('Failed to fetch')) { + errorMessage = '/// ERROR: NETWORK_FAILURE - CHECK_API_ROUTE'; + console.error('[Hubert] Network error - API route may not exist'); + } else { + errorMessage = `/// ERROR: ${error.message}`; + } + } + + setInitError(errorMessage); + setMessages([{ + role: 'system', + content: errorMessage + '\\n\\nCLICK [RETRY] BELOW', + timestamp: new Date().toISOString(), + }]); + } finally { + console.log('[Hubert] Initialization complete, setting isInitializing to false'); + setIsInitializing(false); + } + }; + initVisitor(); + }; + + // Auto-scroll to bottom of chat container (not entire page) useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + if (messagesEndRef.current) { + const container = messagesEndRef.current.parentElement; + if (container) { + container.scrollTop = container.scrollHeight; + } + } }, [messages]); const sendMessage = async () => { - if (!input.trim() || isTyping || !visitorId || !conversationId) return; + console.log('[Hubert] sendMessage called', { input, isTyping, visitorId, conversationId }); + if (!input.trim() || isTyping || !visitorId || !conversationId) { + console.log('[Hubert] sendMessage blocked:', { + noInput: !input.trim(), + isTyping, + noVisitorId: !visitorId, + noConversationId: !conversationId + }); + return; + } + + console.log('[Hubert] Sending message:', input); const userMessage: Message = { role: 'user', content: input, @@ -65,6 +200,7 @@ export default function HubertChat() { setIsTyping(true); try { + console.log('[Hubert] Fetching /api/hubert/chat...'); const response = await fetch('/api/hubert/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -78,21 +214,29 @@ export default function HubertChat() { }), }); + console.log('[Hubert] Response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + const data = await response.json(); - + console.log('[Hubert] Response data:', data); + if (data.error) { throw new Error(data.error); } - + const assistantMessage: Message = { role: 'assistant', content: data.messages[data.messages.length - 1]?.content || '...', timestamp: new Date().toISOString(), }; + console.log('[Hubert] Adding assistant message:', assistantMessage.content); setMessages(prev => [...prev, assistantMessage]); } catch (error) { - console.error('Hubert chat error:', error); + console.error('[Hubert] Chat error:', error); setMessages(prev => [...prev, { role: 'assistant', content: '/// HUBERT_MALFUNCTION - TRY AGAIN', @@ -103,20 +247,53 @@ export default function HubertChat() { } }; - if (isInitializing) { + // Show loading or error state + if (isInitializing || initError) { return (
-
-
-
-
-
-
-
- - HUBERT_IS_BOOTING... - -
+
+ {isInitializing && !initError ? ( + <> +
+
+
+
+
+
+ + HUBERT_IS_BOOTING... + +
+
+ Initializing chatbot... Check console for debug info. +
+ + ) : initError ? ( + <> +
+
+
+
+
+
+ + HUBERT_INITIALIZATION_FAILED + +
+
+ {initError} +
+ +
+ Check browser console for detailed error information. +
+ + ) : null}
); diff --git a/src/components/sections/Hubert.astro b/src/components/sections/Hubert.astro index 9184510..aea0c54 100644 --- a/src/components/sections/Hubert.astro +++ b/src/components/sections/Hubert.astro @@ -35,8 +35,6 @@ import HubertChat from '../HubertChat';
- - - +
diff --git a/src/pages/api/hubert/chat.ts b/src/pages/api/hubert/chat.ts index d49b336..3c6993d 100644 --- a/src/pages/api/hubert/chat.ts +++ b/src/pages/api/hubert/chat.ts @@ -71,7 +71,15 @@ const searchBlog = tool( */ export const POST = async (context) => { try { - const { request, env } = context || {}; + const { request, locals } = context || {}; + // In Astro with Cloudflare adapter, env is at locals.runtime.env + const env = locals?.runtime?.env; + + console.log('[Hubert API] Chat endpoint called'); + console.log('[Hubert API] env object:', env); + console.log('[Hubert API] env keys:', env ? Object.keys(env) : 'no env'); + console.log('[Hubert API] OPENROUTER_API_KEY present:', !!(env?.OPENROUTER_API_KEY)); + const { messages, conversation_id, visitor_id } = await request.json(); if (!messages || !conversation_id || !visitor_id) { @@ -88,7 +96,7 @@ export const POST = async (context) => { const openRouterApiKey = env?.OPENROUTER_API_KEY; if (!openRouterApiKey) { // Dev mode fallback: return a canned response - console.log('[Hubert] Dev mode: No API key, using fallback response'); + console.log('[Hubert API] Dev mode: No API key found, using fallback response'); return new Response( JSON.stringify({ messages: [ @@ -167,7 +175,8 @@ When they say goodbye or conversation ends, use the save_conversation tool to ar const data = await response.json(); const assistantContent = data.choices[0]?.message?.content || '...'; - console.log(`[Hubert] Generated response in ${Date.now() - Date.parse(response.headers.get('date') || '').getTime()}ms`); + const responseTime = response.headers.get('date') ? Date.now() - Date.parse(response.headers.get('date')) : 0; + console.log(`[Hubert] Generated response in ${responseTime}ms`); return new Response( JSON.stringify({ diff --git a/src/pages/api/hubert/new-visitor.ts b/src/pages/api/hubert/new-visitor.ts index 52e0ecf..1bc93e2 100644 --- a/src/pages/api/hubert/new-visitor.ts +++ b/src/pages/api/hubert/new-visitor.ts @@ -9,25 +9,41 @@ export const prerender = false; * Used when Hubert interface first loads */ export const POST = async (context) => { + console.log('[Hubert API] /api/hubert/new-visitor endpoint called'); + try { - const { request, env } = context; + const { request, locals } = context; + // In Astro with Cloudflare adapter, env is at locals.runtime.env + const env = locals?.runtime?.env; + + console.log('[Hubert API] Request received, env binding available:', !!env); + console.log('[Hubert API] HUBERT_DB binding available:', !!(env && env.HUBERT_DB)); + const userAgent = request.headers.get('user-agent') || 'unknown'; const ip = request.headers.get('cf-connecting-ip') || 'unknown'; const visitorId = randomUUID(); + console.log('[Hubert API] Generated visitor ID:', visitorId); // Only insert into database if HUBERT_DB binding exists (production) // In dev mode, this allows the chatbot to work without Cloudflare bindings if (env && env.HUBERT_DB) { - await env.HUBERT_DB.prepare(` - INSERT INTO visitors (visitor_id, first_seen_at, last_seen_at, ip_address, user_agent) - VALUES (?, datetime('now'), datetime('now'), ?, ?) - `).bind(visitorId, ip, userAgent).run(); - console.log(`[Hubert] New visitor initialized: ${visitorId}`); + console.log('[Hubert API] Attempting database insert...'); + try { + await env.HUBERT_DB.prepare(` + INSERT INTO visitors (visitor_id, first_seen_at, last_seen_at, ip_address, user_agent) + VALUES (?, datetime('now'), datetime('now'), ?, ?) + `).bind(visitorId, ip, userAgent).run(); + console.log(`[Hubert API] Database insert successful for visitor: ${visitorId}`); + } catch (dbError) { + console.error('[Hubert API] Database insert failed (continuing anyway):', dbError); + // Continue anyway - don't fail initialization if DB is misconfigured + } } else { - console.log(`[Hubert] Dev mode: Skipping database insert for visitor: ${visitorId}`); + console.log(`[Hubert API] Dev mode: Skipping database insert for visitor: ${visitorId}`); } + console.log('[Hubert API] Returning success response'); return Response.json({ visitor_id: visitorId, conversation_id: visitorId, // Use visitor_id as initial conversation_id diff --git a/wrangler.jsonc b/wrangler.jsonc index 3b21d78..df18c97 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -28,10 +28,14 @@ * Environment Variables * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables */ - // "vars": { "MY_VARIABLE": "production_value" } + "vars": { + "OPENROUTER_API_KEY": "" + }, /** * Note: Use secrets to store sensitive data. * https:// developers.cloudflare.com/workers/configuration/secrets/ + * For dev: Add OPENROUTER_API_KEY to .dev.vars file + * For production: Use wrangler secret put OPENROUTER_API_KEY */ /** * Static Assets