diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fdb357..5eac317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.0.11-alpha] - 2024-09-06 + +### Changed + +- BUI is not a hosted site (can still be run manually from localhost) +- Running `bbai start` will open browser page for hosted BUI, with apiPort pointing to localhost API + + ## [0.0.10b-alpha] - 2024-09-03 ### Changed diff --git a/api/deno.jsonc b/api/deno.jsonc index d717496..dc47b47 100644 --- a/api/deno.jsonc +++ b/api/deno.jsonc @@ -1,6 +1,6 @@ { "name": "bbai-api", - "version": "0.0.10b-alpha", + "version": "0.0.11-alpha", "exports": "./src/main.ts", "tasks": { "start": "deno run --allow-read --allow-write --allow-run --allow-net --allow-env src/main.ts", diff --git a/api/src/llms/tools/rewriteFileTool.ts b/api/src/llms/tools/rewriteFileTool.ts index 34b91a0..f87fde5 100644 --- a/api/src/llms/tools/rewriteFileTool.ts +++ b/api/src/llms/tools/rewriteFileTool.ts @@ -1,5 +1,6 @@ -import { JSX } from 'preact'; -import LLMTool, { LLMToolInputSchema, LLMToolRunResult, LLMToolRunResultContent } from 'api/llms/llmTool.ts'; +import type { JSX } from 'preact'; +import LLMTool from 'api/llms/llmTool.ts'; +import type { LLMToolInputSchema, LLMToolRunResult, LLMToolRunResultContent } from 'api/llms/llmTool.ts'; import { formatToolResult as formatToolResultBrowser, formatToolUse as formatToolUseBrowser, @@ -8,12 +9,12 @@ import { formatToolResult as formatToolResultConsole, formatToolUse as formatToolUseConsole, } from './formatters/rewriteFileTool.console.ts'; -import LLMConversationInteraction from '../interactions/conversationInteraction.ts'; -import ProjectEditor from '../../editor/projectEditor.ts'; +import type LLMConversationInteraction from '../interactions/conversationInteraction.ts'; +import type ProjectEditor from '../../editor/projectEditor.ts'; import { isPathWithinProject } from '../../utils/fileHandling.utils.ts'; import { createError, ErrorType } from '../../utils/error.utils.ts'; -import { FileHandlingErrorOptions } from '../../errors/error.ts'; -import { LLMAnswerToolUse } from 'api/llms/llmMessage.ts'; +import type { FileHandlingErrorOptions } from '../../errors/error.ts'; +import type { LLMAnswerToolUse } from 'api/llms/llmMessage.ts'; import { logger } from 'shared/logger.ts'; import { ensureDir } from '@std/fs'; import { dirname, join } from '@std/path'; @@ -33,7 +34,7 @@ export default class LLMToolRewriteFile extends LLMTool { type: 'object', properties: { filePath: { type: 'string', description: 'The path of the file to be rewritten or created' }, - content: { type: 'string', description: 'The new content of the file' }, + content: { type: 'string', description: 'The new content of the file. Include the full file contents. Do not replace any of the content with comments or placeholders.' }, createIfMissing: { type: 'boolean', description: 'Create the file if it does not exist', @@ -120,7 +121,7 @@ export default class LLMToolRewriteFile extends LLMTool { if (error.name === 'rewrite-file') { throw error; } - let errorMessage = `Failed to write contents to ${filePath}: ${error.message}`; + const errorMessage = `Failed to write contents to ${filePath}: ${error.message}`; logger.error(errorMessage); throw createError(ErrorType.FileHandling, errorMessage, { diff --git a/api/src/main.ts b/api/src/main.ts index 336f626..ba2306c 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -72,8 +72,8 @@ if (environment === 'local') { } app.use(oakCors({ - origin: /^https?:\/\/localhost(:\d+)?$/, -})); // Enable CORS for all localhost requests, regardless of port + origin: [/^https?:\/\/localhost(:\d+)?$/, /^https?:\/\/(chat\.)?bbai\.tips$/], +})); // Enable CORS for localhost, bbai.tips, and chat.bbai.tips app.use(router.routes()); app.use(router.allowedMethods()); diff --git a/bui/deno.jsonc b/bui/deno.jsonc index 59f8188..9dabf32 100644 --- a/bui/deno.jsonc +++ b/bui/deno.jsonc @@ -6,8 +6,7 @@ "manifest": "deno task cli manifest $(pwd)/src", "start": "deno run -A --watch=static/,routes/ src/dev.ts", "start-prod": "deno run -A src/dev.ts", - "build-site": "deno run -A src/dev.ts build", - "build": "deno compile -A --output ../build/bbai-bui src/main.ts", + "build": "deno run -A src/dev.ts build", "preview": "deno run -A src/main.ts", "update": "deno run -A -r https://fresh.deno.dev/update .", "format": "deno fmt", @@ -15,10 +14,22 @@ "check-types": "deno check src/main.ts", "update-deps": "deno cache src/main.ts && deno cache tests/deps.ts" }, - "lint": { "rules": { "tags": ["fresh", "recommended"] } }, - "exclude": ["**/src/_fresh/*"], + "lint": { + "rules": { + "tags": [ + "fresh", + "recommended" + ] + } + }, + "exclude": [ + "**/src/_fresh/*" + ], "importMap": "../import_map.json", - "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, "fmt": { "useTabs": true, "lineWidth": 120, @@ -26,7 +37,14 @@ "semiColons": true, "singleQuote": true, "proseWrap": "preserve", - "include": ["src/", "tests/"], - "exclude": ["src/testdata/", "src/fixtures/**/*.ts"] - } + "include": [ + "src/", + "tests/" + ], + "exclude": [ + "src/testdata/", + "src/fixtures/**/*.ts" + ] + }, + "version": "0.0.11-alpha" } diff --git a/bui/src/islands/Chat.tsx b/bui/src/islands/Chat.tsx index 98234b6..f0409eb 100644 --- a/bui/src/islands/Chat.tsx +++ b/bui/src/islands/Chat.tsx @@ -1,30 +1,30 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { useCallback } from 'preact/hooks'; import { ConversationMetadata } from 'shared/types.ts'; -// Removed unused import -// Removed unused type declaration import { marked } from 'marked'; import { IS_BROWSER } from '$fresh/runtime.ts'; -// import { generateConversationId } from 'shared/conversationManagement.ts'; -// Temporary browser-compatible replacement -const generateConversationId = () => - Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); +import { generateConversationId } from 'shared/conversationManagement.ts'; import { createWebSocketManager } from '../utils/websocketManager.ts'; -// import { ConversationContinue, ConversationEntry, ConversationResponse, ConversationStart } from 'shared/types.ts'; -// Temporary type definitions -type ConversationEntry = any; -type ConversationResponse = any; +import { ConversationContinue, ConversationEntry, ConversationResponse, ConversationStart } from 'shared/types.ts'; +//import type { EventPayloadMap } from 'shared/eventManager.ts'; import { ApiClient } from '../utils/apiClient.utils.ts'; -// Removed import for ConversationsList import { useSignal } from '@preact/signals'; interface ChatProps { - apiPort: number; + //apiPort: number; } -export default function Chat({ apiPort }: ChatProps) { +export default function Chat() { + /* + console.log('Chat component: Received apiPort:', apiPort); + if (typeof window !== 'undefined') { + console.log('Chat component: window.location.href:', window.location.href); + console.log('Chat component: window.location.hash:', window.location.hash); + } + console.log('Chat component: Received apiPort:', apiPort); + */ + const [selectedConversationId, setSelectedConversationId] = useState< string | null >(null); @@ -36,9 +36,6 @@ export default function Chat({ apiPort }: ChatProps) { ); const [isLoadingConversations, setIsLoadingConversations] = useState(false); const [input, setInput] = useState(''); - // Removed conversations state - // Removed selectedConversation state - // Removed isLoadingConversations state const [isLoading, setIsLoading] = useState(false); const [isWorking, setIsWorking] = useState(false); const [error, setError] = useState(null); @@ -47,7 +44,21 @@ export default function Chat({ apiPort }: ChatProps) { setInput(''); }; const messagesEndRef = useRef(null); - const [startDir, setStartDir] = useState(''); + const [apiPort, setApiPort] = useState(() => { + if (IS_BROWSER) { + const hash = window.location.hash.slice(1); // Remove the '#' + const params = new URLSearchParams(hash); + console.log('Chat component: Received apiPort:', params.get('apiPort')); + return params.get('apiPort'); + } + return null; + }); + const [startDir, setStartDir] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('startDir') || ''; + } + return ''; + }); const [conversationId, setConversationId] = useState(null); const wsManager = useSignal | null>( null, @@ -123,15 +134,21 @@ export default function Chat({ apiPort }: ChatProps) { }; useEffect(() => { + console.log('Chat component useEffect. apiPort:', apiPort); console.debug( 'Chat component mounted. IS_BROWSER:', IS_BROWSER, 'apiPort:', apiPort, + 'startDir:', + startDir, ); - if (IS_BROWSER && !wsManager.value && apiPort) { + console.log(`Initializing API client with baseUrl: http://localhost:${apiPort}`); + if (IS_BROWSER && !wsManager.value && apiPort && startDir) { + console.log('Initializing chat with apiPort:', apiPort); const initializeChat = async () => { const baseUrl = `http://localhost:${apiPort}`; + console.log('Chat component: Initializing API client with baseUrl:', baseUrl); apiClient.value = new ApiClient(baseUrl); await fetchConversations(); }; @@ -144,10 +161,6 @@ export default function Chat({ apiPort }: ChatProps) { wsManager.value = manager; manager.connect(); - // ApiClient initialization moved to initializeChat function - - // Removed fetchConversations call - return () => { manager.disconnect(); }; @@ -167,7 +180,7 @@ export default function Chat({ apiPort }: ChatProps) { ]; // Don't set isWorking to false for intermediate entries } else if ('answer' in newEntry) { - conversationEntries.value = [...conversationEntries.value, newEntry]; + conversationEntries.value = [...conversationEntries.value, newEntry as ConversationResponse]; // Only set isWorking to false when we receive the final answer setIsWorking(false); } else if ('conversationTitle' in newEntry) { @@ -175,25 +188,21 @@ export default function Chat({ apiPort }: ChatProps) { } // Update current conversation metadata if (currentConversation) { - setCurrentConversation({ - ...prevConversation, - title: 'conversationTitle' in newEntry - ? newEntry.conversationTitle || prevConversation?.title || 'Untitled' - : prevConversation?.title || 'Untitled', - updatedAt: new Date().toISOString(), - - conversationStats: { - ...(prevConversation?.conversationStats || {}), - conversationTurnCount: - ((prevConversation?.conversationStats?.conversationTurnCount ?? 0) + 1), - statementCount: prevConversation?.conversationStats?.statementCount ?? 0, - statementTurnCount: prevConversation?.conversationStats?.statementTurnCount ?? 0, - providerRequestCount: prevConversation?.conversationStats?.providerRequestCount ?? 0, - }, - tokenUsageConversation: 'tokenUsageConversation' in newEntry - ? (newEntry.tokenUsageConversation || prevConversation?.tokenUsageConversation) - : { inputTokensTotal: 0, outputTokensTotal: 0, totalTokensTotal: 0 }, - }); + if (currentConversation) { + setCurrentConversation({ + ...currentConversation, + title: newEntry.conversationTitle || currentConversation.title, + updatedAt: new Date().toISOString(), + //conversationStats: { + // ...currentConversation.conversationStats, + // conversationTurnCount: + // (currentConversation.conversationStats?.conversationTurnCount || 0) + + // 1, + //}, + tokenUsageConversation: newEntry.tokenUsageConversation || + currentConversation.tokenUsageConversation, + }); + } } }); @@ -306,20 +315,20 @@ export default function Chat({ apiPort }: ChatProps) { }); setInput(''); // Update current conversation metadata - if (currentConversation) { - setCurrentConversation((prev) => - prev - ? ({ - ...prev, - updatedAt: new Date().toISOString(), - conversationStats: { - ...prev.conversationStats, - conversationTurnCount: (prev.conversationStats?.conversationTurnCount ?? 0) + 1, - }, - }) - : {} - ); - } + //if (currentConversation) { + // setCurrentConversation((prev) => + // prev + // ? ({ + // ...prev, + // updatedAt: new Date().toISOString(), + // conversationStats: { + // ...prev.conversationStats, + // conversationTurnCount: (prev.conversationStats?.conversationTurnCount ?? 0) + 1, + // }, + // }) + // : {} + // ); + //} } catch (error) { console.error('Error sending message:', error); console.error('WebSocket manager state at error:', wsManager.value); @@ -460,7 +469,7 @@ export default function Chat({ apiPort }: ChatProps) { className='bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4' role='alert' > - Error: + Generic Error: {error} { - setStartDir(e.currentTarget.value); + const newStartDir = e.currentTarget.value; + setStartDir(newStartDir); + localStorage.setItem('startDir', newStartDir); wsManager.value?.setStartDir(e.currentTarget.value); fetchConversations(); }} diff --git a/bui/src/routes/_app.tsx b/bui/src/routes/_app.tsx index f4b2bab..4747a11 100644 --- a/bui/src/routes/_app.tsx +++ b/bui/src/routes/_app.tsx @@ -1,11 +1,36 @@ import { PageProps } from '$fresh/server.ts'; -import { config } from 'shared/configManager.ts'; +// Removed import of useEffect and useState +//import { config } from 'shared/configManager.ts'; import { Head } from '$fresh/runtime.ts'; -export default function App({ Component, state }: PageProps) { - const apiPort = config.api?.apiPort ?? 8000; // Fallback to 8000 if not defined - state.apiPort = apiPort; - console.log('_app.tsx: apiPort =', state.apiPort); +export default function App({ Component, url }: PageProps) { +/* + console.log('_app.tsx: URL object:', url); + if (typeof window !== 'undefined') { + //console.log('_app.tsx: window.location.href:', window.location.href); + //console.log('_app.tsx: window.location.hash:', window.location.hash); + } + console.log('_app.tsx: URL hash:', url.hash); + let apiPort = null; + if (typeof window !== 'undefined') { + // Client-side parsing + const hash = window.location.hash.slice(1); // Remove the '#' + const params = new URLSearchParams(hash); + apiPort = params.get('apiPort'); + } else { + // Server-side parsing + apiPort = url.hash ? new URLSearchParams(url.hash.slice(1)).get('apiPort') : null; + } + console.log('_app.tsx: Parsed apiPort:', apiPort); + console.log(`_app.tsx: Parsed apiPort: ${apiPort}`); + + // Log on both server and client side + if (typeof window !== 'undefined') { + console.log('_app.tsx (Client): Using apiPort:', apiPort); + } else { + console.log('_app.tsx (Server): Using apiPort:', apiPort); + } + */ return ( @@ -15,7 +40,7 @@ export default function App({ Component, state }: PageProps) { - + ); diff --git a/bui/src/routes/index.tsx b/bui/src/routes/index.tsx index 509399b..2646541 100644 --- a/bui/src/routes/index.tsx +++ b/bui/src/routes/index.tsx @@ -1,20 +1,18 @@ import { PageProps } from '$fresh/server.ts'; -// Remove useState import as it's not needed in this file import Chat from '../islands/Chat.tsx'; // Removed import for ConversationsList interface HomeProps { - apiPort: number; + //apiPort: number; } export default function Home(props: PageProps) { - // Remove state management from this component //console.log("index.tsx: props =", props); - const { apiPort } = props.state; + //const { apiPort } = props.state; //console.log("index.tsx: apiPort =", apiPort); return (
- +
); } diff --git a/bui/src/utils/websocketManager.ts b/bui/src/utils/websocketManager.ts index 9ce9792..4ad65e9 100644 --- a/bui/src/utils/websocketManager.ts +++ b/bui/src/utils/websocketManager.ts @@ -17,8 +17,10 @@ class WebSocketManager { private maxReconnectAttempts = 5; private reconnectDelay = 1000; - private conversationEntriesSignal = signal([]); - private subscribers: ((messages: ConversationEntry[]) => void)[] = []; + //private conversationEntriesSignal = signal([]); + //private subscribers: ((messages: ConversationEntry[]) => void)[] = []; + private conversationEntriesSignal = signal({} as ConversationEntry); + private subscribers: ((message: ConversationEntry) => void)[] = []; public isConnected = signal(false); public isReady = signal(false); public error = signal(null); @@ -70,7 +72,8 @@ class WebSocketManager { ) { if ('answer' in msg.data || 'logEntry' in msg.data) { //this.conversationEntriesSignal.value = [...this.conversationEntriesSignal.value, msg.data as ConversationEntry]; - this.conversationEntriesSignal.value = [msg.data as ConversationEntry]; + //this.conversationEntriesSignal.value = [msg.data as ConversationEntry]; + this.conversationEntriesSignal.value = msg.data as ConversationEntry; } else if ('conversationTitle' in msg.data) { // Handle conversationReady this.isReady.value = true; @@ -121,7 +124,8 @@ class WebSocketManager { } } - subscribe(callback: (messages: ConversationEntry[]) => void) { + //subscribe(callback: (messages: ConversationEntry[]) => void) { + subscribe(callback: (message: ConversationEntry) => void) { this.subscribers.push(callback); return { unsubscribe: () => { @@ -135,7 +139,8 @@ class WebSocketManager { this.subscribers.forEach((callback) => callback(entries)); } - get conversationEntries(): Signal { + //get conversationEntries(): Signal { + get conversationEntries(): Signal { return this.conversationEntriesSignal; } } diff --git a/cli/deno.jsonc b/cli/deno.jsonc index 1509cca..e10df12 100644 --- a/cli/deno.jsonc +++ b/cli/deno.jsonc @@ -1,6 +1,6 @@ { "name": "bbai-cli", - "version": "0.0.10b-alpha", + "version": "0.0.11-alpha", "exports": "./src/main.ts", "tasks": { "start": "deno run --allow-read --allow-write --allow-run --allow-net src/main.ts", diff --git a/cli/src/commands/apiStart.ts b/cli/src/commands/apiStart.ts index c57c0b2..9308249 100644 --- a/cli/src/commands/apiStart.ts +++ b/cli/src/commands/apiStart.ts @@ -1,4 +1,6 @@ import { Command } from 'cliffy/command/mod.ts'; + +import { config } from 'shared/configManager.ts'; import { followApiLogs, startApiServer } from '../utils/apiControl.utils.ts'; export const apiStart = new Command() @@ -10,12 +12,30 @@ export const apiStart = new Command() .action(async ({ logLevel: apiLogLevel, logFile: apiLogFile, follow }) => { const startDir = Deno.cwd(); const { pid, apiLogFilePath } = await startApiServer(startDir, apiLogLevel, apiLogFile, follow); + const apiPort = config.api?.apiPort || 3000; + + //const chatUrl = `https://chat.bbai.tips/#apiPort=${apiPort}`; + const chatUrl = `https://bbai.tips/#apiPort=${apiPort}`; + + try { + const command = Deno.build.os === 'windows' + ? new Deno.Command('cmd', { args: ['/c', 'start', chatUrl] }) + : Deno.build.os === 'darwin' + ? new Deno.Command('open', { args: [chatUrl] }) + : new Deno.Command('xdg-open', { args: [chatUrl] }); + await command.output(); + } catch (error) { + console.error('Failed to open the browser automatically. Please open the URL manually.', error); + } if (follow) { await followApiLogs(apiLogFilePath, startDir); } else { console.log(`API server started with PID: ${pid}`); console.log(`Logs are being written to: ${apiLogFilePath}`); + console.log(`\n\x1b[1m\x1b[32mBBai API started successfully!\x1b[0m`); + console.log(`\x1b[1m\x1b[36mPlease visit: ${chatUrl}\x1b[0m`); + console.log('Attempting to open the chat in your default browser...'); console.log("Use 'bbai api stop' to stop the server."); Deno.exit(0); } diff --git a/deno.jsonc b/deno.jsonc index ede7acd..b210aaa 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "bbai", - "version": "0.0.10b-alpha", + "version": "0.0.11-alpha", "exports": "./cli/src/main.ts", "tasks": { "tool:check-types-project": "deno task -c ./cli/deno.jsonc check-types && deno task -c ./bui/deno.jsonc check-types && deno task -c ./api/deno.jsonc check-types", @@ -15,8 +15,7 @@ "start-api": "deno task -c ./api/deno.jsonc start", "start-api-dev": "deno task -c ./api/deno.jsonc dev", "build": "deno task -c ./cli/deno.jsonc build && deno task -c ./api/deno.jsonc build", - "build-all": "deno task -c ./cli/deno.jsonc build && deno task -c ./api/deno.jsonc build && deno task -c ./bui/deno.jsonc build-compile", - "homebrew": "deno task -c ./cli/deno.jsonc build && deno task -c ./bui/deno.jsonc build && deno task -c ./api/deno.jsonc build", + "homebrew": "deno task -c ./cli/deno.jsonc build && deno task -c ./api/deno.jsonc build", "test": "deno task -c ./cli/deno.jsonc test && deno task -c ./bui/deno.jsonc test && deno task -c ./api/deno.jsonc test", "update-deps": "deno task -c ./cli/deno.jsonc update-deps && deno task -c ./bui/deno.jsonc update-deps && deno task -c ./api/deno.jsonc update-deps", "update-version": "deno run --allow-read --allow-write --allow-run ./scripts/update_version.ts" @@ -29,7 +28,15 @@ "semiColons": true, "singleQuote": true, "proseWrap": "preserve", - "include": ["src/", "cli/src/", "bui/src/", "api/src/"], - "exclude": ["src/testdata/", "src/fixtures/**/*.ts"] + "include": [ + "src/", + "cli/src/", + "bui/src/", + "api/src/" + ], + "exclude": [ + "src/testdata/", + "src/fixtures/**/*.ts" + ] } } diff --git a/version.ts b/version.ts index 6d5d8ec..7334b1d 100644 --- a/version.ts +++ b/version.ts @@ -1 +1 @@ -export const VERSION = "0.0.10b-alpha"; \ No newline at end of file +export const VERSION = "0.0.11-alpha"; \ No newline at end of file