set up frontend/ infra
This commit is contained in:
parent
c511cefc67
commit
fa66066c07
|
|
@ -16,3 +16,4 @@ appmap.log
|
|||
*.swp
|
||||
/vsc/node_modules
|
||||
/vsc/dist
|
||||
node_modules/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "@ra-aid/common",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Entry point for @ra-aid/common package
|
||||
export const hello = (): void => {
|
||||
console.log("Hello from @ra-aid/common");
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "frontend-monorepo",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"common",
|
||||
"web",
|
||||
"vsc"
|
||||
],
|
||||
"scripts": {
|
||||
"install-all": "npm install",
|
||||
"dev:web": "npm --workspace @ra-aid/web run dev"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
|
@ -0,0 +1,140 @@
|
|||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/extension.ts
|
||||
var extension_exports = {};
|
||||
__export(extension_exports, {
|
||||
activate: () => activate,
|
||||
deactivate: () => deactivate
|
||||
});
|
||||
module.exports = __toCommonJS(extension_exports);
|
||||
var vscode = __toESM(require("vscode"));
|
||||
var RAWebviewViewProvider = class {
|
||||
constructor(_extensionUri) {
|
||||
this._extensionUri = _extensionUri;
|
||||
}
|
||||
/**
|
||||
* Called when a view is first created to initialize the webview
|
||||
*/
|
||||
resolveWebviewView(webviewView, context, _token) {
|
||||
webviewView.webview.options = {
|
||||
// Enable JavaScript in the webview
|
||||
enableScripts: true,
|
||||
// Restrict the webview to only load resources from the extension's directory
|
||||
localResourceRoots: [this._extensionUri]
|
||||
};
|
||||
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
|
||||
}
|
||||
/**
|
||||
* Creates HTML content for the webview with proper security policies
|
||||
*/
|
||||
_getHtmlForWebview(webview) {
|
||||
const logoUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "RA.png"));
|
||||
const nonce = getNonce();
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RA.Aid</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
color: var(--vscode-foreground);
|
||||
font-size: var(--vscode-font-size);
|
||||
font-weight: var(--vscode-font-weight);
|
||||
font-family: var(--vscode-font-family);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-size: 1.3em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p {
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="${logoUri}" alt="RA.Aid Logo" class="logo">
|
||||
<h1>RA.Aid</h1>
|
||||
<p>Your research and development assistant.</p>
|
||||
<p>More features coming soon!</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
};
|
||||
function getNonce() {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function activate(context) {
|
||||
console.log('Congratulations, your extension "ra-aid" is now active!');
|
||||
const provider = new RAWebviewViewProvider(context.extensionUri);
|
||||
const viewRegistration = vscode.window.registerWebviewViewProvider(
|
||||
"ra-aid.view",
|
||||
// Must match the view id in package.json
|
||||
provider
|
||||
);
|
||||
context.subscriptions.push(viewRegistration);
|
||||
const disposable = vscode.commands.registerCommand("ra-aid.helloWorld", () => {
|
||||
vscode.window.showInformationMessage("Hello World from RA.Aid!");
|
||||
});
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
function deactivate() {
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
});
|
||||
//# sourceMappingURL=extension.js.map
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/extension.ts"],
|
||||
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,aAAwB;AAKxB,IAAM,wBAAN,MAAkE;AAAA,EAChE,YAA6B,eAA2B;AAA3B;AAAA,EAA4B;AAAA;AAAA;AAAA;AAAA,EAKlD,mBACL,aACA,SACA,QACA;AAEA,gBAAY,QAAQ,UAAU;AAAA;AAAA,MAE5B,eAAe;AAAA;AAAA,MAEf,oBAAoB,CAAC,KAAK,aAAa;AAAA,IACzC;AAGA,gBAAY,QAAQ,OAAO,KAAK,mBAAmB,YAAY,OAAO;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA,EAKQ,mBAAmB,SAAiC;AAE1D,UAAM,UAAU,QAAQ,aAAoB,WAAI,SAAS,KAAK,eAAe,UAAU,QAAQ,CAAC;AAMhG,UAAM,QAAQ,SAAS;AAEvB,WAAO;AAAA;AAAA;AAAA;AAAA,0FAI+E,QAAQ,SAAS,sBAAsB,QAAQ,SAAS,uCAAuC,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAsCxK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3B;AACF;AAKA,SAAS,WAAW;AAClB,MAAI,OAAO;AACX,QAAM,WAAW;AACjB,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAQ,SAAS,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,SAAS,MAAM,CAAC;AAAA,EACrE;AACA,SAAO;AACT;AAGO,SAAS,SAAS,SAAkC;AAEzD,UAAQ,IAAI,yDAAyD;AAGrE,QAAM,WAAW,IAAI,sBAAsB,QAAQ,YAAY;AAC/D,QAAM,mBAA0B,cAAO;AAAA,IACrC;AAAA;AAAA,IACA;AAAA,EACF;AACA,UAAQ,cAAc,KAAK,gBAAgB;AAK3C,QAAM,aAAoB,gBAAS,gBAAgB,qBAAqB,MAAM;AAG5E,IAAO,cAAO,uBAAuB,0BAA0B;AAAA,EACjE,CAAC;AAED,UAAQ,cAAc,KAAK,UAAU;AACvC;AAGO,SAAS,aAAa;AAAC;",
|
||||
"names": []
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>@ra-aid/web</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@ra-aid/web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"@ra-aid/common": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.0.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { hello } from '@ra-aid/common';
|
||||
|
||||
hello();
|
||||
|
||||
const App = () => (
|
||||
<div>
|
||||
<h1>Hello from @ra-aid/web using Vite</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
// Ensure that Vite treats symlinked packages as local, so HMR works correctly.
|
||||
alias: {
|
||||
'@ra-aid/common': path.resolve(__dirname, '../common/src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
// Watch for changes in the common package.
|
||||
// This pattern forces Vite to notice file changes in the shared library.
|
||||
// Adjust the pattern if your common package layout is different.
|
||||
paths: ['../common/src/**']
|
||||
}
|
||||
}
|
||||
});
|
||||
109
webui/README.md
109
webui/README.md
|
|
@ -1,109 +0,0 @@
|
|||
# RA.Aid Web Interface
|
||||
|
||||
A modern, dark-themed web interface for RA.Aid that provides:
|
||||
- Beautiful dark mode chat interface
|
||||
- Real-time streaming output with syntax highlighting
|
||||
- Persistent request history with quick resubmission
|
||||
- Responsive design that works on all devices
|
||||
|
||||
## Design
|
||||
|
||||
The interface features a modern dark theme using Tailwind CSS with:
|
||||
- Sleek dark color scheme optimized for readability
|
||||
- Clean, minimalist design focused on content
|
||||
- Smooth transitions and hover effects
|
||||
- Monospace font for code and output display
|
||||
|
||||
## Features
|
||||
|
||||
- **Left Sidebar**: Shows history of previous requests
|
||||
- **Main Chat Area**: Displays conversation and streaming output
|
||||
- **Input Area**: Text input for new requests
|
||||
- **Real-time Updates**: See RA.Aid output as it happens
|
||||
- **Error Handling**: Clear display of any errors that occur
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the required dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Make sure you have RA.Aid installed in your Python environment
|
||||
|
||||
3. Start the web server:
|
||||
```bash
|
||||
# Default: Listen on all interfaces (0.0.0.0) port 8080
|
||||
./server.py
|
||||
|
||||
# Specify custom port
|
||||
./server.py --port 3000
|
||||
|
||||
# Specify custom host and port
|
||||
./server.py --host 127.0.0.1 --port 3000
|
||||
```
|
||||
|
||||
4. Open your web browser and navigate to the server address:
|
||||
```
|
||||
# If running locally with default settings:
|
||||
http://localhost:8080
|
||||
|
||||
# If running on a different port:
|
||||
http://localhost:<port>
|
||||
|
||||
# If accessing from another machine:
|
||||
http://<server-ip>:<port>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Type your request in the input box at the bottom of the screen
|
||||
2. Press Enter or click the Send button to submit
|
||||
3. Watch the real-time output in the chat area
|
||||
4. Previous requests appear in the left sidebar
|
||||
5. Click any previous request to load it into the input box
|
||||
|
||||
## Development
|
||||
|
||||
The web interface consists of:
|
||||
- `index.html`: Main page layout with integrated Tailwind CSS styling
|
||||
- `script.js`: Client-side functionality and WebSocket handling
|
||||
- `server.py`: FastAPI WebSocket server with RA.Aid integration
|
||||
- `requirements.txt`: Python server dependencies
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**:
|
||||
- Tailwind CSS for modern, utility-first styling
|
||||
- Vanilla JavaScript for lightweight client-side operations
|
||||
- WebSocket for real-time communication
|
||||
|
||||
- **Backend**:
|
||||
- FastAPI for high-performance async server
|
||||
- WebSocket support for real-time streaming
|
||||
- Direct integration with ra-aid CLI
|
||||
- Concurrent stdout/stderr handling
|
||||
- Error handling and status reporting
|
||||
|
||||
## Command Integration
|
||||
|
||||
The web interface integrates with ra-aid by:
|
||||
- Executing `ra-aid -m "<message>" --cowboy-mode` for each request
|
||||
- Streaming real-time output through WebSocket connection
|
||||
- Handling both standard output and error streams
|
||||
- Providing error feedback for failed commands
|
||||
|
||||
For example, when you type "What time is it?" in the interface, it executes:
|
||||
```bash
|
||||
ra-aid -m "What time is it?" --cowboy-mode
|
||||
```
|
||||
|
||||
The output is streamed in real-time to the web interface, maintaining the same functionality as the CLI but with a modern web interface.
|
||||
|
||||
## Architecture
|
||||
|
||||
- FastAPI backend with WebSocket support
|
||||
- Static file serving for web assets
|
||||
- Real-time bi-directional communication
|
||||
- Integration with RA.Aid's output streaming
|
||||
- Browser-based frontend with vanilla JavaScript
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-gray-900">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="server-port" content="{{ server_port }}">
|
||||
<title>RA.Aid Web Interface</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'dark-primary': '#1a1b26',
|
||||
'dark-secondary': '#24283b',
|
||||
'dark-accent': '#7aa2f7',
|
||||
'dark-text': '#c0caf5'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-dark-primary text-dark-text">
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-64 bg-dark-secondary border-r border-gray-700 flex flex-col">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-dark-accent">History</h2>
|
||||
</div>
|
||||
<div id="history-list" class="flex-1 overflow-y-auto p-4 space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Chat Container -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="chat-container">
|
||||
<div id="chat-messages"></div>
|
||||
<div id="stream-output" class="hidden font-mono bg-dark-secondary rounded-lg p-4 text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="border-t border-gray-700 p-4 bg-dark-secondary">
|
||||
<div class="flex space-x-4">
|
||||
<textarea
|
||||
id="user-input"
|
||||
class="flex-1 bg-dark-primary border border-gray-700 rounded-lg p-3 text-dark-text placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-dark-accent resize-none"
|
||||
placeholder="Type your request here..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
<button
|
||||
id="send-button"
|
||||
class="px-6 py-2 bg-dark-accent text-white rounded-lg hover:bg-opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-dark-accent disabled:opacity-50 disabled:cursor-not-allowed h-fit"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add dynamic styles for messages
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.message {
|
||||
@apply mb-4 p-4 rounded-lg max-w-3xl;
|
||||
}
|
||||
.user-message {
|
||||
@apply bg-dark-accent text-white ml-auto;
|
||||
}
|
||||
.system-message {
|
||||
@apply bg-dark-secondary mr-auto;
|
||||
}
|
||||
.error-message {
|
||||
@apply bg-red-900 text-red-100 mr-auto;
|
||||
}
|
||||
.history-item {
|
||||
@apply p-3 rounded-lg hover:bg-dark-primary cursor-pointer transition-colors duration-200 text-sm;
|
||||
}
|
||||
#stream-output:not(:empty) {
|
||||
@apply block;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
websockets>=12.0
|
||||
jinja2>=3.1.2
|
||||
163
webui/script.js
163
webui/script.js
|
|
@ -1,163 +0,0 @@
|
|||
class RAWebUI {
|
||||
constructor() {
|
||||
this.messageHistory = [];
|
||||
this.setupElements();
|
||||
this.setupEventListeners();
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
setupElements() {
|
||||
this.userInput = document.getElementById('user-input');
|
||||
this.sendButton = document.getElementById('send-button');
|
||||
this.chatMessages = document.getElementById('chat-messages');
|
||||
this.streamOutput = document.getElementById('stream-output');
|
||||
this.historyList = document.getElementById('history-list');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.sendButton.addEventListener('click', () => this.sendMessage());
|
||||
this.userInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async connectWebSocket() {
|
||||
try {
|
||||
// Get the server port from the response header or default to 8080
|
||||
const serverPort = document.querySelector('meta[name="server-port"]')?.content || '8080';
|
||||
|
||||
// Construct WebSocket URL using the server port
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.hostname}:${serverPort}/ws`;
|
||||
console.log('Attempting to connect to WebSocket URL:', wsUrl);
|
||||
|
||||
console.log('Creating new WebSocket connection...');
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connection established successfully');
|
||||
console.log('Connected to WebSocket server');
|
||||
this.sendButton.disabled = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('Disconnected from WebSocket server');
|
||||
this.sendButton.disabled = true;
|
||||
setTimeout(() => this.connectWebSocket(), 5000);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleServerMessage(data);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to WebSocket:', error);
|
||||
setTimeout(() => this.connectWebSocket(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
handleServerMessage(data) {
|
||||
if (data.type === 'stream_start') {
|
||||
this.streamOutput.textContent = '';
|
||||
this.streamOutput.style.display = 'block';
|
||||
} else if (data.type === 'stream_end') {
|
||||
this.streamOutput.style.display = 'none';
|
||||
this.addToHistory(data.request);
|
||||
} else if (data.type === 'chunk') {
|
||||
this.handleChunk(data.chunk);
|
||||
}
|
||||
}
|
||||
|
||||
handleChunk(chunk) {
|
||||
if (chunk.agent && chunk.agent.messages) {
|
||||
chunk.agent.messages.forEach(msg => {
|
||||
if (msg.content) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
msg.content.forEach(content => {
|
||||
if (content.type === 'text' && content.text.trim()) {
|
||||
this.appendMessage(content.text.trim(), 'system');
|
||||
}
|
||||
});
|
||||
} else if (msg.content.trim()) {
|
||||
this.appendMessage(msg.content.trim(), 'system');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (chunk.tools && chunk.tools.messages) {
|
||||
chunk.tools.messages.forEach(msg => {
|
||||
if (msg.status === 'error' && msg.content) {
|
||||
this.appendMessage(msg.content.trim(), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
appendMessage(content, type) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}-message`;
|
||||
messageDiv.textContent = content;
|
||||
this.chatMessages.appendChild(messageDiv);
|
||||
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
addToHistory(request) {
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.textContent = request.slice(0, 50) + (request.length > 50 ? '...' : '');
|
||||
historyItem.title = request;
|
||||
historyItem.addEventListener('click', () => {
|
||||
this.userInput.value = request;
|
||||
this.userInput.focus();
|
||||
});
|
||||
this.historyList.insertBefore(historyItem, this.historyList.firstChild);
|
||||
this.messageHistory.push(request);
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
console.log('Send button clicked');
|
||||
const message = this.userInput.value.trim();
|
||||
console.log('Message content:', message);
|
||||
|
||||
if (!message) {
|
||||
console.log('Message is empty, not sending');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('WebSocket state:', this.ws.readyState);
|
||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket is not connected');
|
||||
this.appendMessage('Error: WebSocket is not connected. Trying to reconnect...', 'error');
|
||||
this.connectWebSocket();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Sending message to server');
|
||||
this.appendMessage(message, 'user');
|
||||
const payload = { type: 'request', content: message };
|
||||
console.log('Payload:', payload);
|
||||
|
||||
this.ws.send(JSON.stringify(payload));
|
||||
console.log('Message sent successfully');
|
||||
|
||||
this.userInput.value = '';
|
||||
this.sendButton.disabled = true;
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
this.appendMessage(`Error sending message: ${error.message}`, 'error');
|
||||
this.sendButton.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the UI when the page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.raWebUI = new RAWebUI();
|
||||
});
|
||||
210
webui/server.py
210
webui/server.py
|
|
@ -1,210 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
# Verify ra-aid is available
|
||||
if not shutil.which("ra-aid"):
|
||||
print(
|
||||
"Error: ra-aid command not found. Please ensure it's installed and in your PATH"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, replace with specific origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Setup templates
|
||||
templates = Jinja2Templates(directory=Path(__file__).parent)
|
||||
|
||||
|
||||
# Create a route for the root to serve index.html with port parameter
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def get_root(request: Request):
|
||||
"""Serve the index.html file with port parameter."""
|
||||
return templates.TemplateResponse(
|
||||
"index.html", {"request": request, "server_port": request.url.port or 8080}
|
||||
)
|
||||
|
||||
|
||||
# Mount static files for js and other assets
|
||||
app.mount("/static", StaticFiles(directory=Path(__file__).parent), name="static")
|
||||
|
||||
# Store WebSocket connections
|
||||
|
||||
# Store active WebSocket connections
|
||||
active_connections: List[WebSocket] = []
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
print(f"New WebSocket connection from {websocket.client}")
|
||||
await websocket.accept()
|
||||
print("WebSocket connection accepted")
|
||||
active_connections.append(websocket)
|
||||
|
||||
try:
|
||||
while True:
|
||||
print("Waiting for message...")
|
||||
message = await websocket.receive_json()
|
||||
print(f"Received message: {message}")
|
||||
|
||||
if message["type"] == "request":
|
||||
print(f"Processing request: {message['content']}")
|
||||
# Notify client that streaming is starting
|
||||
await websocket.send_json({"type": "stream_start"})
|
||||
|
||||
try:
|
||||
# Run ra-aid with the request using -m flag and cowboy mode
|
||||
cmd = ["ra-aid", "-m", message["content"], "--cowboy-mode"]
|
||||
print(f"Executing command: {' '.join(cmd)}")
|
||||
|
||||
# Create subprocess
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
print(f"Process started with PID: {process.pid}")
|
||||
|
||||
# Read output and errors concurrently
|
||||
async def read_stream(stream, is_error=False):
|
||||
stream_type = "stderr" if is_error else "stdout"
|
||||
print(f"Starting to read from {stream_type}")
|
||||
while True:
|
||||
line = await stream.readline()
|
||||
if not line:
|
||||
print(f"End of {stream_type} stream")
|
||||
break
|
||||
|
||||
try:
|
||||
decoded_line = line.decode().strip()
|
||||
print(f"{stream_type} line: {decoded_line}")
|
||||
if decoded_line:
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools" if is_error else "agent": {
|
||||
"messages": [
|
||||
{
|
||||
"content": decoded_line,
|
||||
"status": (
|
||||
"error"
|
||||
if is_error
|
||||
else "info"
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error sending output: {e}")
|
||||
|
||||
# Create tasks for reading stdout and stderr
|
||||
stdout_task = asyncio.create_task(read_stream(process.stdout))
|
||||
stderr_task = asyncio.create_task(read_stream(process.stderr, True))
|
||||
|
||||
# Wait for both streams to complete
|
||||
await asyncio.gather(stdout_task, stderr_task)
|
||||
|
||||
# Wait for process to complete
|
||||
return_code = await process.wait()
|
||||
|
||||
if return_code != 0:
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools": {
|
||||
"messages": [
|
||||
{
|
||||
"content": f"Process exited with code {return_code}",
|
||||
"status": "error",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Notify client that streaming is complete
|
||||
await websocket.send_json(
|
||||
{"type": "stream_end", "request": message["content"]}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error executing ra-aid: {str(e)}"
|
||||
print(error_msg)
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "chunk",
|
||||
"chunk": {
|
||||
"tools": {
|
||||
"messages": [
|
||||
{"content": error_msg, "status": "error"}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
print("WebSocket client disconnected")
|
||||
active_connections.remove(websocket)
|
||||
except Exception as e:
|
||||
print(f"WebSocket error: {e}")
|
||||
try:
|
||||
await websocket.send_json({"type": "error", "error": str(e)})
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if websocket in active_connections:
|
||||
active_connections.remove(websocket)
|
||||
print("WebSocket connection cleaned up")
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(request: Request):
|
||||
"""Return server configuration including host and port."""
|
||||
return {"host": request.client.host, "port": request.scope.get("server")[1]}
|
||||
|
||||
|
||||
def run_server(host: str = "0.0.0.0", port: int = 8080):
|
||||
"""Run the FastAPI server."""
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="RA.Aid Web Interface Server")
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=8080, help="Port to listen on (default: 8080)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Host to listen on (default: 0.0.0.0)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
run_server(host=args.host, port=args.port)
|
||||
Loading…
Reference in New Issue