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
This commit is contained in:
parent
e1a1a4259a
commit
b49b5d1df4
16
.claude/.fuse_hidden009df63e00000355
Normal file
16
.claude/.fuse_hidden009df63e00000355
Normal file
@ -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": []
|
||||
}
|
||||
}
|
||||
@ -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<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
@ -14,18 +31,35 @@ export default function HubertChat() {
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [initError, setInitError] = useState<string | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="bg-[var(--theme-bg-secondary)] border-2 border-[var(--theme-border-primary)] shadow-2xl">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-2 h-2 bg-brand-accent animate-pulse" />
|
||||
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
|
||||
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-[var(--theme-text-muted)]">
|
||||
HUBERT_IS_BOOTING...
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center py-12 px-6 gap-6">
|
||||
{isInitializing && !initError ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-2 h-2 bg-brand-accent animate-pulse" />
|
||||
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
|
||||
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-[var(--theme-text-muted)]">
|
||||
HUBERT_IS_BOOTING...
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-[var(--theme-text-subtle)] text-center max-w-md">
|
||||
Initializing chatbot... Check console for debug info.
|
||||
</div>
|
||||
</>
|
||||
) : initError ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-2 h-2 bg-red-500" />
|
||||
<div className="w-2 h-2 bg-red-500/50" />
|
||||
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-red-400">
|
||||
HUBERT_INITIALIZATION_FAILED
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-sm text-[var(--theme-text-muted)] text-center max-w-md px-4 py-3 bg-[var(--theme-bg-tertiary)] border border-[var(--theme-border-secondary)]">
|
||||
{initError}
|
||||
</div>
|
||||
<button
|
||||
onClick={retryInit}
|
||||
className="px-6 py-3 bg-brand-accent text-brand-dark font-mono text-[10px] uppercase tracking-widest font-bold hover:bg-brand-accent/90 transition-all border-none cursor-pointer"
|
||||
>
|
||||
[RETRY]
|
||||
</button>
|
||||
<div className="text-[10px] font-mono text-[var(--theme-text-subtle)] text-center max-w-md">
|
||||
Check browser console for detailed error information.
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -35,8 +35,6 @@ import HubertChat from '../HubertChat';
|
||||
</div>
|
||||
|
||||
<!-- Hubert Chat Interface -->
|
||||
<client:load>
|
||||
<HubertChat />
|
||||
</client:load>
|
||||
<HubertChat client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user