diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..80cf75e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(rm:*)", + "Bash(yarn build)", + "Bash(grep:*)", + "Bash(yarn run:*)", + "Bash(mv:*)", + "Bash(mkdir:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE-agentic-kit.md b/CLAUDE-agentic-kit.md new file mode 100644 index 0000000..204c96d --- /dev/null +++ b/CLAUDE-agentic-kit.md @@ -0,0 +1,231 @@ +# Agentic Kit + +Agentic Kit is the core library providing a unified, streaming-capable interface for multiple LLM providers. It lets you plug in any supported adapter and switch between them at runtime. + +## Installation + +```bash +npm install agentic-kit +# or +yarn add agentic-kit +``` + +Agentic Kit includes adapters for Ollama and Bradie out of the box. + +## Quick Start + +```typescript +import { + // Adapters + OllamaAdapter, + BradieAdapter, + + // Factory functions + createOllamaKit, + createBradieKit, + createMultiProviderKit, + + // Core type + AgentKit +} from 'agentic-kit'; + +// Ollama-only client +const ollamaKit: AgentKit = createOllamaKit('http://localhost:11434'); +const text = await ollamaKit.generate({ model: 'mistral', prompt: 'Hello' }); +console.log(text); + +// Bradie-only client +const bradieKit: AgentKit = createBradieKit({ + domain: 'http://localhost:3000', + onSystemMessage: (msg) => console.log('[system]', msg), + onAssistantReply: (msg) => console.log('[assistant]', msg), +}); +await bradieKit.generate({ prompt: 'Hello' }); + +// Multi-provider client +const multiKit = createMultiProviderKit(); +multiKit.addProvider(new OllamaAdapter('http://localhost:11434')); +multiKit.addProvider(new BradieAdapter({ + domain: 'http://localhost:3000', + onSystemMessage: console.log, + onAssistantReply: console.log +})); +const reply = await multiKit.generate({ model: 'mistral', prompt: 'Hello' }); +console.log(reply); +``` + +## Streaming + +Both adapters support a streaming mode that invokes a callback for each data chunk. + +```typescript +await ollamaKit.generate( + { model: 'mistral', prompt: 'Hello', stream: true }, + (chunk) => console.log('Ollama chunk:', chunk) +); + +await bradieKit.generate( + { model: 'mistral', prompt: 'Hello', stream: true }, + (chunk) => console.log('Bradie chunk:', chunk) +); +``` + +## API Reference + +### AgentKit + +- `.generate(input: GenerateInput, options?: StreamingOptions): Promise` +- `.listProviders(): string[]` +- `.setProvider(name: string): void` +- `.getCurrentProvider(): AgentProvider | undefined` + +### GenerateInput + +```ts +interface GenerateInput { + model?: string; // For services that support named models + prompt: string; // The text prompt + stream?: boolean; // If true, use streaming mode +} +``` + +### StreamingOptions + +```ts +interface StreamingOptions { + onChunk?: (chunk: string) => void; + onStateChange?: (state: string) => void; + onError?: (error: Error) => void; +} +``` + +Source of the main class + +```ts +import { Bradie, BradieState } from '@agentic-kit/bradie'; +import OllamaClient, { GenerateInput } from '@agentic-kit/ollama'; + +export interface AgentProvider { + name: string; + generate(input: GenerateInput): Promise; + generateStreaming(input: GenerateInput, onChunk: (chunk: string) => void): Promise; +} + +export interface StreamingOptions { + onChunk?: (chunk: string) => void; + onStateChange?: (state: string) => void; + onError?: (error: Error) => void; +} + +export class OllamaAdapter implements AgentProvider { + public readonly name = 'ollama'; + private client: OllamaClient; + + constructor(baseUrl?: string) { + this.client = new OllamaClient(baseUrl); + } + + async generate(input: GenerateInput): Promise { + return this.client.generate(input); + } + + async generateStreaming(input: GenerateInput, onChunk: (chunk: string) => void): Promise { + await this.client.generate({ ...input, stream: true }, onChunk); + } +} + +export class BradieAdapter implements AgentProvider { + public readonly name = 'bradie'; + private client: Bradie; + + constructor(config: { + domain: string; + onSystemMessage: (msg: string) => void; + onAssistantReply: (msg: string) => void; + onError?: (err: Error) => void; + onComplete?: () => void; + }) { + this.client = new Bradie(config); + } + + async generate(input: GenerateInput): Promise { + const requestId = await this.client.sendMessage(input.prompt); + return new Promise((resolve, reject) => { + this.client.subscribeToResponse(requestId) + .then(() => resolve('Response completed')) + .catch(reject); + }); + } + + async generateStreaming(input: GenerateInput, onChunk: (chunk: string) => void): Promise { + const requestId = await this.client.sendMessage(input.prompt); + await this.client.subscribeToResponse(requestId); + } + + getState(): BradieState { + return this.client.getState(); + } +} + +export class AgentKit { + private providers: Map = new Map(); + private currentProvider?: AgentProvider; + + addProvider(provider: AgentProvider): void { + this.providers.set(provider.name, provider); + if (!this.currentProvider) { + this.currentProvider = provider; + } + } + + setProvider(name: string): void { + const provider = this.providers.get(name); + if (!provider) { + throw new Error(`Provider '${name}' not found`); + } + this.currentProvider = provider; + } + + getCurrentProvider(): AgentProvider | undefined { + return this.currentProvider; + } + + async generate(input: GenerateInput, options?: StreamingOptions): Promise { + if (!this.currentProvider) { + throw new Error('No provider set'); + } + + if (input.stream && options?.onChunk) { + return this.currentProvider.generateStreaming(input, options.onChunk); + } + + return this.currentProvider.generate(input); + } + + listProviders(): string[] { + return Array.from(this.providers.keys()); + } +} + +export function createOllamaKit(baseUrl?: string): AgentKit { + const kit = new AgentKit(); + kit.addProvider(new OllamaAdapter(baseUrl)); + return kit; +} + +export function createBradieKit(config: { + domain: string; + onSystemMessage: (msg: string) => void; + onAssistantReply: (msg: string) => void; + onError?: (err: Error) => void; + onComplete?: () => void; +}): AgentKit { + const kit = new AgentKit(); + kit.addProvider(new BradieAdapter(config)); + return kit; +} + +export function createMultiProviderKit(): AgentKit { + return new AgentKit(); +} +``` diff --git a/CLAUDE-bradie.md b/CLAUDE-bradie.md new file mode 100644 index 0000000..1ca42bc --- /dev/null +++ b/CLAUDE-bradie.md @@ -0,0 +1,443 @@ +# Bradie Client + +A Node.js client for the Bradie LLM service, providing project initialization, messaging, streaming responses, and log retrieval. + +## Installation + +```bash +npm install @agentic-kit/bradie +# or +yarn add @agentic-kit/bradie +``` + +## Usage + +```typescript +import { Bradie } from '@agentic-kit/bradie'; + +// 1. Configure callbacks and create client +const client = new Bradie({ + domain: 'http://localhost:3001', + onSystemMessage: (msg) => console.log('[system]', msg), + onAssistantReply: (msg) => console.log('[assistant]', msg), + onError: (err) => console.error('[error]', err), + onComplete: () => console.log('[complete]'), +}); + +// 2. Initialize a project (creates session & project IDs) +const { sessionId, projectId } = await client.initProject( + 'my-project', + '/path/to/project' +); +console.log('Session ID:', sessionId, 'Project ID:', projectId); + +// 3. Send a message and get a request ID +const requestId = await client.sendMessage('Hello, Bradie!'); +console.log('Request ID:', requestId); + +// 4. Subscribe to streaming logs (chat messages & system events) +await client.subscribeToResponse(requestId); +``` + +### fetchOnce + +Retrieve the complete array of command logs for a given request: + +```typescript +const logs = await client.fetchOnce(requestId); +console.log(logs); +``` + + +Source of the main class + +```ts +import fetch from 'cross-fetch'; +import { Buffer } from 'buffer'; + +export type BradieState = 'idle' | 'thinking' | 'streaming' | 'complete' | 'error'; + +export type Command = + | 'read_file' + | 'modify_file' + | 'create_file' + | 'search_file' + | 'chat' + | 'analyze' + | 'delete_file' + | 'run_code'; + +export interface AgentCommandLog { + command: Command; + error: string | null; + parameters?: Record; + message?: string; + result?: any; +} + +export type ActionLog = + | { type: 'assistant'; content: string } + | { type: 'system'; content: string } + | { type: 'error'; content: string } + | { type: 'complete'; content?: string }; + +interface InstanceIdResponse { instanceId: string; } +interface InitResponse { sessionId: string; projectId: string; } +interface ActResponse { + act_json: AgentCommandLog[]; + status: BradieState; + error?: string; +} + +export class Bradie { + private domain: string; + private onSystemMessage: (msg: string) => void; + private onAssistantReply: (msg: string) => void; + private onError?: (err: Error) => void; + private onComplete?: () => void; + private state: BradieState = 'idle'; + private sessionId?: string; + private projectId?: string; + private seenLogs: Set = new Set(); + + constructor(config: { + domain: string; + onSystemMessage: (msg: string) => void; + onAssistantReply: (msg: string) => void; + onError?: (err: Error) => void; + onComplete?: () => void; + }) { + this.domain = config.domain; + this.onSystemMessage = config.onSystemMessage; + this.onAssistantReply = config.onAssistantReply; + this.onError = config.onError; + this.onComplete = config.onComplete; + } + + public getState(): BradieState { + return this.state; + } + + public async initProject( + projectName: string, + projectPath: string + ): Promise<{ sessionId: string; projectId: string }> { + const res1 = await fetch(`${this.domain}/api/instance-id`); + if (!res1.ok) { + const bodyText = await res1.text(); + let parsed: any; + try { parsed = JSON.parse(bodyText); } catch {} + const details = parsed?.error ?? parsed?.message ?? bodyText; + throw new Error(`Failed to get instance ID: ${res1.status} ${res1.statusText} - ${details}`); + } + const { instanceId }: InstanceIdResponse = await res1.json(); + + const res2 = await fetch(`${this.domain}/api/init`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ instanceId, projectName, projectPath }), + }); + if (!res2.ok) { + const bodyText = await res2.text(); + let parsed: any; + try { parsed = JSON.parse(bodyText); } catch {} + const details = parsed?.error ?? parsed?.message ?? bodyText; + throw new Error(`Failed to init project: ${res2.status} ${res2.statusText} - ${details}`); + } + const { sessionId, projectId }: InitResponse = await res2.json(); + this.sessionId = sessionId; + this.projectId = projectId; + return { sessionId, projectId }; + } + + public async sendMessage(message: string): Promise { + if (!this.sessionId) throw new Error('Project not initialized'); + this.state = 'thinking'; + const res = await fetch(`${this.domain}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId: this.sessionId, message }), + }); + if (!res.ok) { + const bodyText = await res.text(); + let parsed: any; + try { parsed = JSON.parse(bodyText); } catch {} + const details = parsed?.error ?? parsed?.message ?? bodyText; + throw new Error(`Failed to send message: ${res.status} ${res.statusText} - ${details}`); + } + const data: { requestId: string; message: string; image?: string } = await res.json(); + return data.requestId; + } + + public async subscribeToResponse( + requestId: string, + opts?: { pollInterval?: number; maxPolls?: number } + ): Promise { + if (!this.sessionId) throw new Error('Project not initialized'); + const interval = opts?.pollInterval ?? 1000; + const maxPolls = opts?.maxPolls ?? Infinity; + let polls = 0; + + return new Promise((resolve, reject) => { + const poll = async () => { + try { + const res = await fetch( + `${this.domain}/api/act?sessionId=${this.sessionId}&requestId=${requestId}` + ); + if (!res.ok) { + let details: string; + try { + details = await res.text(); + } catch { + details = 'No response body'; + } + throw new Error(`Poll failed: ${res.status} ${res.statusText} - ${details}`); + } + const data: ActResponse = await res.json(); + this.state = data.status; + + for (const log of data.act_json) { + const key = JSON.stringify(log); + if (!this.seenLogs.has(key)) { + this.seenLogs.add(key); + if (log.error) { + this.state = 'error'; + this.onError?.(new Error(log.error)); + return reject(new Error(log.error)); + } else if (log.command === 'chat') { + const content = log.parameters?.message || log.message || ''; + this.onAssistantReply(content); + } else { + this.onSystemMessage( + `✅ ${log.command}: ${log.message ?? JSON.stringify(log.result)}` + ); + } + } + } + + if (data.status === 'complete') { + this.state = 'complete'; + this.onComplete?.(); + return resolve(); + } else if (data.status === 'error') { + this.state = 'error'; + this.onError?.(new Error(data.error || 'Unknown error')); + return reject(new Error(data.error)); + } + + polls++; + if (polls >= maxPolls) { + return reject(new Error('Max polls exceeded')); + } + setTimeout(poll, interval); + } catch (err: any) { + this.state = 'error'; + this.onError?.(err); + return reject(err); + } + }; + poll(); + }); + } + + public async fetchOnce(requestId: string): Promise { + if (!this.sessionId) throw new Error('Project not initialized'); + const res = await fetch( + `${this.domain}/api/act?sessionId=${this.sessionId}&requestId=${requestId}` + ); + if (!res.ok) throw new Error(`Failed to fetch logs: ${res.statusText}`); + const data: ActResponse = await res.json(); + return data.act_json; + } + + // --- API Wrapper Methods --- + + public async getInstanceId(): Promise<{ instanceId: string; port: number }> { + const res = await fetch(`${this.domain}/api/instance-id`); + if (!res.ok) throw new Error(`getInstanceId failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async getInstanceInfo(): Promise<{ instance_id: string; backend_port: number; frontend_port?: number }> { + const res = await fetch(`${this.domain}/api/instance-info`); + if (!res.ok) throw new Error(`getInstanceInfo failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async health(): Promise { + const res = await fetch(`${this.domain}/api/health`); + if (!res.ok) throw new Error(`health check failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async getFileTree(sessionId: string, instanceId: string): Promise { + const url = new URL(`${this.domain}/api/files/tree`); + url.searchParams.append('sessionId', sessionId); + url.searchParams.append('instanceId', instanceId); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`getFileTree failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async readFile(sessionId: string, filePath: string): Promise<{ content: string }> { + const url = new URL(`${this.domain}/api/files/read`); + url.searchParams.append('sessionId', sessionId); + url.searchParams.append('path', filePath); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`readFile failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async writeFile(sessionId: string, filePath: string, content: string): Promise<{ success: boolean }> { + const res = await fetch(`${this.domain}/api/files/write`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId, path: filePath, content }), + }); + if (!res.ok) throw new Error(`writeFile failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async terminalStatus(sessionId: string): Promise<{ status: string; project_path: string }> { + const url = new URL(`${this.domain}/api/terminal-status`); + url.searchParams.append('sessionId', sessionId); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`terminalStatus failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async getMode(): Promise<{ mode: string; projectName: string | null; projectPath: string | null }> { + const res = await fetch(`${this.domain}/api/mode`); + if (!res.ok) throw new Error(`getMode failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async listProjects(): Promise> { + const res = await fetch(`${this.domain}/api/projects`); + if (!res.ok) throw new Error(`listProjects failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async getProject(projectId: string): Promise { + const res = await fetch(`${this.domain}/api/project/${projectId}`); + if (!res.ok) throw new Error(`getProject failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async getProjectSummary(sessionId: string): Promise { + const url = new URL(`${this.domain}/api/project_summary`); + url.searchParams.append('sessionId', sessionId); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`getProjectSummary failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async getStatus(sessionId: string): Promise { + const url = new URL(`${this.domain}/api/status`); + url.searchParams.append('sessionId', sessionId); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`getStatus failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async postStatus(sessionId: string): Promise { + const res = await fetch(`${this.domain}/api/status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId }), + }); + if (!res.ok) throw new Error(`postStatus failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async recoverMessages(sessionId: string): Promise { + const url = new URL(`${this.domain}/api/recover_message`); + url.searchParams.append('sessionId', sessionId); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`recoverMessages failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async transcribe(sessionId: string, file: Blob | Buffer | File): Promise<{ text: string }> { + const form = new FormData(); + form.append('sessionId', sessionId); + form.append('file', file as any); + const res = await fetch(`${this.domain}/api/transcribe`, { + method: 'POST', + body: form, + }); + if (!res.ok) throw new Error(`transcribe failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async tts(text: string): Promise { + const res = await fetch(`${this.domain}/api/tts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text }), + }); + if (!res.ok) throw new Error(`tts failed: ${res.status} ${res.statusText}`); + const arrayBuffer = await res.arrayBuffer(); + return Buffer.from(arrayBuffer); + } + + public async offlineStatus(): Promise<'available' | 'unavailable'> { + const res = await fetch(`${this.domain}/api/offline-status`); + if (!res.ok) throw new Error(`offlineStatus failed: ${res.status} ${res.statusText}`); + const data = await res.json(); + return data.status; + } + + public async getProjectRepo(sessionId: string): Promise<{ path: string; url: string }> { + const url = new URL(`${this.domain}/api/project_repo`); + url.searchParams.append('sessionId', sessionId); + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`getProjectRepo failed: ${res.status} ${res.statusText}`); + return res.json(); + } + + public async stopAgent(sessionId: string): Promise { + const res = await fetch(`${this.domain}/api/stop_agent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId }), + }); + if (!res.ok) throw new Error(`stopAgent failed: ${res.status} ${res.statusText}`); + const data = await res.json(); + return data.success; + } + + public async resetAgent(sessionId: string): Promise { + const res = await fetch(`${this.domain}/api/reset_agent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId }), + }); + if (!res.ok) throw new Error(`resetAgent failed: ${res.status} ${res.statusText}`); + const data = await res.json(); + return data.success; + } + + public async feedback(sessionId: string, feedback: Record): Promise { + const res = await fetch(`${this.domain}/api/feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId, ...feedback }), + }); + if (!res.ok) throw new Error(`feedback failed: ${res.status} ${res.statusText}`); + const data = await res.json(); + return data.success; + } + + public async feedbackMessage(sessionId: string, messageId: string, feedback: Record): Promise { + const res = await fetch(`${this.domain}/api/feedback/message`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId, messageId, ...feedback }), + }); + if (!res.ok) throw new Error(`feedbackMessage failed: ${res.status} ${res.statusText}`); + const data = await res.json(); + return data.success; + } +} +``` \ No newline at end of file diff --git a/CLAUDE-ollama.md b/CLAUDE-ollama.md new file mode 100644 index 0000000..f582132 --- /dev/null +++ b/CLAUDE-ollama.md @@ -0,0 +1,292 @@ +# Ollama Client + +A JavaScript/TypeScript client for the Ollama LLM server, supporting model listing, text generation, streaming responses, embeddings, and model management. + +## Installation + +```bash +npm install @agentic-kit/ollama +# or +yarn add @agentic-kit/ollama +``` + +## Usage + +```typescript +import OllamaClient, { GenerateInput } from '@agentic-kit/ollama'; + +// Create a client (default port 11434) +const client = new OllamaClient('http://localhost:11434'); + +// List available models +const models = await client.listModels(); +console.log('Available models:', models); + +// Non-streaming text generation +const output = await client.generate({ model: 'mistral', prompt: 'Hello, Ollama!' }); +console.log(output); + +// Streaming generation +await client.generate( + { model: 'mistral', prompt: 'Hello, streaming!', stream: true }, + (chunk) => { + console.log('Received chunk:', chunk); + } +); + +// Pull a model to local cache +await client.pullModel('mistral'); + +// Generate embeddings +const embedding = await client.generateEmbedding('Compute embeddings'); +console.log('Embedding vector length:', embedding.length); + +// Generate a conversational response with context +const response = await client.generateResponse( + 'What is the capital of France?', + 'Geography trivia' +); +console.log(response); + +// Delete a pulled model when done +await client.deleteModel('mistral'); +``` + +## API Reference + +- `new OllamaClient(baseUrl?: string)` – defaults to `http://localhost:11434` +- `.listModels(): Promise` +- `.generate(input: GenerateInput, onChunk?: (chunk: string) => void): Promise` +- `.generateStreamingResponse(prompt: string, onChunk: (chunk: string) => void, context?: string): Promise` +- `.generateEmbedding(text: string): Promise` +- `.generateResponse(prompt: string, context?: string): Promise` +- `.pullModel(model: string): Promise` +- `.deleteModel(model: string): Promise` + +## GenerateInput type + +```ts +interface GenerateInput { + model: string; + prompt: string; + stream?: boolean; +} +``` + +Source of the main class + +```ts +import fetch from 'cross-fetch'; + +export interface GenerateInput { + model: string; + prompt: string; + stream?: boolean; +} + +interface TagsResponse { + tags: string[]; +} + +interface GenerateResponse { + text: string; +} + +interface OllamaResponse { + model: string; + created_at: string; + response: string; + done: boolean; +} + +interface OllamaEmbeddingResponse { + embedding: number[]; +} + +export default class OllamaClient { + private baseUrl: string; + + constructor(baseUrl: string = 'http://localhost:11434') { + this.baseUrl = baseUrl; + } + + public async listModels(): Promise { + const response = await fetch(`${this.baseUrl}/api/tags`); + if (!response.ok) { + throw new Error(`Failed to list models: ${response.status} ${response.statusText}`); + } + const data: TagsResponse = await response.json(); + return data.tags; + } + + public async generate(input: GenerateInput): Promise; + public async generate(input: GenerateInput, onChunk: (chunk: string) => void): Promise; + public async generate( + input: GenerateInput, + onChunk?: (chunk: string) => void + ): Promise { + if (input.stream && onChunk) { + return this.generateStreamingResponse(input.prompt, onChunk); + } + + const response = await fetch(`${this.baseUrl}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...input, + stream: input.stream || false + }), + }); + if (!response.ok) { + let errorText: string; + try { + const errorData = await response.json(); + errorText = errorData.message ?? JSON.stringify(errorData); + } catch { + errorText = await response.text(); + } + throw new Error(`Generate request failed: ${response.status} ${response.statusText} - ${errorText}`); + } + const data: GenerateResponse = await response.json(); + return data.text; + } + + public async pullModel(model: string): Promise { + const response = await fetch(`${this.baseUrl}/api/pull`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: model }), + }); + if (!response.ok) { + let errorText: string; + try { + const errorData = await response.json(); + errorText = errorData.message ?? JSON.stringify(errorData); + } catch { + errorText = await response.text(); + } + throw new Error(`Pull model failed: ${response.status} ${response.statusText} - ${errorText}`); + } + } + + public async deleteModel(model: string): Promise { + const response = await fetch(`${this.baseUrl}/api/delete`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: model }), + }); + if (!response.ok) { + let errorText: string; + try { + const errorData = await response.json(); + errorText = errorData.message ?? JSON.stringify(errorData); + } catch { + errorText = await response.text(); + } + throw new Error(`Delete model failed: ${response.status} ${response.statusText} - ${errorText}`); + } + } + + public async generateEmbedding(text: string): Promise { + const response = await fetch(`${this.baseUrl}/api/embeddings`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'mistral', + prompt: text, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to generate embedding: ${response.statusText}`); + } + + const data: OllamaEmbeddingResponse = await response.json(); + return data.embedding; + } + + public async generateResponse(prompt: string, context?: string): Promise { + const fullPrompt = context + ? `Context: ${context}\n\nQuestion: ${prompt}\n\nAnswer:` + : prompt; + + const response = await fetch(`${this.baseUrl}/api/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'mistral', + prompt: fullPrompt, + stream: false, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to generate response: ${response.statusText}`); + } + + const data: OllamaResponse = await response.json(); + return data.response; + } + + public async generateStreamingResponse( + prompt: string, + onChunk: (chunk: string) => void, + context?: string + ): Promise { + const fullPrompt = context + ? `Context: ${context}\n\nQuestion: ${prompt}\n\nAnswer:` + : prompt; + + const response = await fetch(`${this.baseUrl}/api/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'mistral', + prompt: fullPrompt, + stream: true, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to generate streaming response: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('Failed to get response reader'); + } + + const decoder = new TextDecoder(); + let done = false; + while (!done) { + const result = await reader.read(); + done = result.done; + if (done) break; + const value = result.value; + + if (value) { + const chunk = decoder.decode(value); + const lines = chunk.split('\n').filter(Boolean); + + for (const line of lines) { + try { + const data: OllamaResponse = JSON.parse(line); + if (data.response) { + onChunk(data.response); + } + } catch (error) { + console.error('Error parsing chunk:', error); + } + } + } + } + } +} + +``` \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8ab4354 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- `yarn dev` - Start development server on http://localhost:3000 +- `yarn build` - Build for production +- `yarn start` - Start production server +- `yarn lint` - Run ESLint checks +- `yarn codegen` - Generate code from TypeScript schemas/configs + +## Prerequisites for Development + +The application requires a running Kubernetes cluster and kubectl proxy: +```bash +kubectl proxy --port=8001 --accept-hosts='^.*$' --address='0.0.0.0' +``` + +## Architecture Overview + +### Core Structure +This is a Next.js 14 React application that provides a modern dashboard for managing Kubernetes resources. The app uses a multi-layered provider architecture: + +1. **KubernetesProvider** (`k8s/context.tsx`) - Manages the Kubernetes client connection and configuration, wraps the entire app with TanStack Query for data fetching +2. **NamespaceProvider** (`contexts/NamespaceContext.tsx`) - Handles global namespace state across all components +3. **ConfirmProvider** (`hooks/useConfirm.tsx`) - Provides confirmation dialogs for destructive operations + +### Key Components +- **DashboardLayout** (`components/dashboard-layout.tsx`) - Main layout with collapsible sidebar navigation, theme toggle, and integrated AI chat +- **Resources** (`components/resources/`) - Individual resource management components for each Kubernetes resource type +- **IDE Components** (`components/ide/`) - File explorer, terminal, and AI chat for integrated development experience + +### Data Layer +- **Kubernetes Client** - Uses `kubernetesjs` library for API communication +- **TanStack Query** - Handles data fetching, caching, and synchronization +- **Custom Hooks** (`hooks/`) - Resource-specific hooks (useDeployments, usePods, etc.) that wrap TanStack Query + +### Navigation & Routing +The sidebar navigation is organized into collapsible sections: +- Overview & All Resources +- Workloads (Deployments, Pods, Jobs, etc.) +- Config & Storage (ConfigMaps, Secrets, PVCs, etc.) +- Network (Services, Ingresses, Network Policies, etc.) +- Access Control (Service Accounts, Roles, Role Bindings) +- Cluster (Resource Quotas, HPAs, Events, etc.) + +### API Routes +- `/api/k8s/[...path]` - Proxies requests to Kubernetes API +- `/api/ide/fs/` - File system operations for IDE functionality +- `/api/init` - Application initialization + +### Styling +- Uses Tailwind CSS with custom design system +- Radix UI components for accessible primitives +- Light/dark theme support +- Responsive design with mobile-friendly sidebar + +### Key Dependencies +- `@kubernetesjs/react` & `kubernetesjs` - Kubernetes API client +- `@tanstack/react-query` - Data fetching and state management +- `@monaco-editor/react` - Code editor for YAML editing +- `xterm` - Terminal emulator for IDE functionality + +### new items +use agentic-kit to switch between providers, these files have the info you need: +- CLAUDE-agentic-kit.md +- CLAUDE-bradie.md +- CLAUDE-ollama.md diff --git a/app/api/ide/fs/list/route.ts b/app/api/ide/fs/list/route.ts new file mode 100644 index 0000000..06398d9 --- /dev/null +++ b/app/api/ide/fs/list/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server' +import { promises as fs } from 'fs' +import path from 'path' + +interface FileInfo { + name: string + path: string + type: 'file' | 'directory' + size?: number + modified?: string +} + +// Get the project root directory (where the Next.js app is running) +const PROJECT_ROOT = process.cwd() + +// Ensure path is within project bounds +function isPathSafe(requestedPath: string): boolean { + const resolvedPath = path.resolve(PROJECT_ROOT, requestedPath) + return resolvedPath.startsWith(PROJECT_ROOT) +} + +export async function POST(request: NextRequest) { + try { + const { dirPath = '.' } = await request.json() + + // Validate path + if (!isPathSafe(dirPath)) { + return NextResponse.json( + { error: 'Invalid path: Access denied' }, + { status: 403 } + ) + } + + const fullPath = path.join(PROJECT_ROOT, dirPath) + + // Check if directory exists + const stats = await fs.stat(fullPath) + if (!stats.isDirectory()) { + return NextResponse.json( + { error: 'Path is not a directory' }, + { status: 400 } + ) + } + + // Read directory contents + const entries = await fs.readdir(fullPath) + const files: FileInfo[] = [] + + // Get stats for each entry + for (const entry of entries) { + // Skip hidden files and common ignore patterns + if (entry.startsWith('.') || entry === 'node_modules') { + continue + } + + try { + const entryPath = path.join(fullPath, entry) + const entryStats = await fs.stat(entryPath) + + files.push({ + name: entry, + path: path.relative(PROJECT_ROOT, entryPath), + type: entryStats.isDirectory() ? 'directory' : 'file', + size: entryStats.size, + modified: entryStats.mtime.toISOString() + }) + } catch (error) { + // Skip files we can't access + console.error(`Error accessing ${entry}:`, error) + } + } + + // Sort: directories first, then alphabetically + files.sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1 + } + return a.name.localeCompare(b.name) + }) + + return NextResponse.json({ files, basePath: dirPath }) + } catch (error) { + console.error('Error listing directory:', error) + return NextResponse.json( + { error: 'Failed to list directory' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/ide/fs/read/route.ts b/app/api/ide/fs/read/route.ts new file mode 100644 index 0000000..cb50aaf --- /dev/null +++ b/app/api/ide/fs/read/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from 'next/server' +import { promises as fs } from 'fs' +import path from 'path' + +const PROJECT_ROOT = process.cwd() + +// Ensure path is within project bounds +function isPathSafe(requestedPath: string): boolean { + const resolvedPath = path.resolve(PROJECT_ROOT, requestedPath) + return resolvedPath.startsWith(PROJECT_ROOT) +} + +// Determine if file is likely binary +function isBinaryFile(filePath: string): boolean { + const binaryExtensions = [ + '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', + '.pdf', '.zip', '.tar', '.gz', '.rar', + '.exe', '.dll', '.so', '.dylib', + '.mp3', '.mp4', '.avi', '.mov', + '.woff', '.woff2', '.ttf', '.eot' + ] + + const ext = path.extname(filePath).toLowerCase() + return binaryExtensions.includes(ext) +} + +export async function POST(request: NextRequest) { + try { + const { filePath } = await request.json() + + if (!filePath) { + return NextResponse.json( + { error: 'File path is required' }, + { status: 400 } + ) + } + + // Validate path + if (!isPathSafe(filePath)) { + return NextResponse.json( + { error: 'Invalid path: Access denied' }, + { status: 403 } + ) + } + + const fullPath = path.join(PROJECT_ROOT, filePath) + + // Check if file exists + const stats = await fs.stat(fullPath) + if (!stats.isFile()) { + return NextResponse.json( + { error: 'Path is not a file' }, + { status: 400 } + ) + } + + // Check if file is binary + if (isBinaryFile(fullPath)) { + return NextResponse.json({ + content: '', + binary: true, + message: 'Binary file - cannot display content' + }) + } + + // Read file content + const content = await fs.readFile(fullPath, 'utf-8') + + return NextResponse.json({ + content, + binary: false, + size: stats.size, + modified: stats.mtime.toISOString() + }) + } catch (error) { + console.error('Error reading file:', error) + return NextResponse.json( + { error: 'Failed to read file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/ide/fs/stat/route.ts b/app/api/ide/fs/stat/route.ts new file mode 100644 index 0000000..560c3b6 --- /dev/null +++ b/app/api/ide/fs/stat/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server' +import { promises as fs } from 'fs' +import path from 'path' + +const PROJECT_ROOT = process.cwd() + +// Ensure path is within project bounds +function isPathSafe(requestedPath: string): boolean { + const resolvedPath = path.resolve(PROJECT_ROOT, requestedPath) + return resolvedPath.startsWith(PROJECT_ROOT) +} + +export async function POST(request: NextRequest) { + try { + const { filePath } = await request.json() + + if (!filePath) { + return NextResponse.json( + { error: 'File path is required' }, + { status: 400 } + ) + } + + // Validate path + if (!isPathSafe(filePath)) { + return NextResponse.json( + { error: 'Invalid path: Access denied' }, + { status: 403 } + ) + } + + const fullPath = path.join(PROJECT_ROOT, filePath) + + // Get file stats + const stats = await fs.stat(fullPath) + + return NextResponse.json({ + exists: true, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + size: stats.size, + created: stats.birthtime.toISOString(), + modified: stats.mtime.toISOString(), + accessed: stats.atime.toISOString() + }) + } catch (error: any) { + if (error.code === 'ENOENT') { + return NextResponse.json({ exists: false }) + } + + console.error('Error getting file stats:', error) + return NextResponse.json( + { error: 'Failed to get file stats' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/ide/fs/write/route.ts b/app/api/ide/fs/write/route.ts new file mode 100644 index 0000000..7752828 --- /dev/null +++ b/app/api/ide/fs/write/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' +import { promises as fs } from 'fs' +import path from 'path' + +const PROJECT_ROOT = process.cwd() + +// Ensure path is within project bounds +function isPathSafe(requestedPath: string): boolean { + const resolvedPath = path.resolve(PROJECT_ROOT, requestedPath) + return resolvedPath.startsWith(PROJECT_ROOT) +} + +// Check if file should be writable +function isWritable(filePath: string): boolean { + // Prevent writing to critical files + const restricted = [ + 'package.json', + 'package-lock.json', + 'yarn.lock', + 'next.config.js', + 'tsconfig.json', + '.env', + '.env.local' + ] + + const fileName = path.basename(filePath) + return !restricted.includes(fileName) +} + +export async function POST(request: NextRequest) { + try { + const { filePath, content } = await request.json() + + if (!filePath || content === undefined) { + return NextResponse.json( + { error: 'File path and content are required' }, + { status: 400 } + ) + } + + // Validate path + if (!isPathSafe(filePath)) { + return NextResponse.json( + { error: 'Invalid path: Access denied' }, + { status: 403 } + ) + } + + // Check if file is writable + if (!isWritable(filePath)) { + return NextResponse.json( + { error: 'File is protected and cannot be modified' }, + { status: 403 } + ) + } + + const fullPath = path.join(PROJECT_ROOT, filePath) + + // Ensure directory exists + const dir = path.dirname(fullPath) + await fs.mkdir(dir, { recursive: true }) + + // Write file + await fs.writeFile(fullPath, content, 'utf-8') + + // Get updated stats + const stats = await fs.stat(fullPath) + + return NextResponse.json({ + success: true, + size: stats.size, + modified: stats.mtime.toISOString() + }) + } catch (error) { + console.error('Error writing file:', error) + return NextResponse.json( + { error: 'Failed to write file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/init/route.ts b/app/api/init/route.ts new file mode 100644 index 0000000..8a3df5a --- /dev/null +++ b/app/api/init/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server' +import { randomUUID } from 'crypto' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { projectName, projectPath, instanceId } = body + + // Validate required fields + if (!projectName || !projectPath || !instanceId) { + return NextResponse.json( + { error: 'Missing required fields: projectName, projectPath, or instanceId' }, + { status: 400 } + ) + } + + // In a real implementation, this would: + // 1. Create a project in a database + // 2. Initialize any necessary resources + // 3. Set up file system access for the project path + // 4. Create a session for the user + + // For now, we'll simulate project initialization + const sessionId = randomUUID() + const projectId = randomUUID() + + console.log('Initializing project:', { + projectName, + projectPath, + instanceId, + sessionId, + projectId + }) + + // Return the session and project IDs + return NextResponse.json({ + sessionId, + projectId, + projectName, + projectPath + }) + } catch (error) { + console.error('Failed to initialize project:', error) + return NextResponse.json( + { error: 'Failed to initialize project' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/instance-id/route.ts b/app/api/instance-id/route.ts new file mode 100644 index 0000000..4c68680 --- /dev/null +++ b/app/api/instance-id/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server' +import { randomUUID } from 'crypto' + +// In a real implementation, this would be stored in a database or retrieved from a service +let instanceId: string | null = null + +export async function GET() { + try { + // Generate instance ID if not exists + if (!instanceId) { + instanceId = randomUUID() + } + + return NextResponse.json({ instanceId }) + } catch (error) { + console.error('Failed to get instance ID:', error) + return NextResponse.json( + { error: 'Failed to get instance ID' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/contracts/page.tsx b/app/contracts/page.tsx new file mode 100644 index 0000000..213f130 --- /dev/null +++ b/app/contracts/page.tsx @@ -0,0 +1,38 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { FileCode2 } from 'lucide-react' + +export default function ContractsPage() { + return ( +
+
+

Contracts

+

+ Deploy and manage smart contracts. +

+
+ +
+ + +
+ +
+
+ Smart Contracts + + Contract deployment and management + +
+
+ +

+ Coming soon - deploy and manage smart contracts +

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/chains/page.tsx b/app/d/chains/page.tsx new file mode 100644 index 0000000..3b6b84a --- /dev/null +++ b/app/d/chains/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Link, Plus, Activity, Globe, Zap } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function ChainsPage() { + return ( +
+
+
+

Blockchains

+

+ Manage your connected blockchain networks and nodes +

+
+ +
+ + {/* Stats Cards */} +
+ + + Connected Chains + + + +
8
+

6 mainnet, 2 testnet

+
+
+ + + Active Nodes + + + +
24
+

All nodes healthy

+
+
+ + + Networks + + + +
5
+

Ethereum, Polygon, BSC, +2

+
+
+ + + Avg Block Time + + + +
2.3s
+

Across all chains

+
+
+
+ + {/* Chains List */} + + + Chain Connections + + Monitor blockchain network connections and node health + + + +
+ +

Blockchain chain management interface coming soon

+

Connect to multiple blockchain networks and monitor node health

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/contracts/page.tsx b/app/d/contracts/page.tsx new file mode 100644 index 0000000..da38e2f --- /dev/null +++ b/app/d/contracts/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { FileCode2, Plus, Activity, CheckCircle, AlertCircle } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function ContractsPage() { + return ( +
+
+
+

Smart Contracts

+

+ Manage your deployed smart contracts and ABIs +

+
+ +
+ + {/* Stats Cards */} +
+ + + Deployed Contracts + + + +
42
+

Across 8 chains

+
+
+ + + Active Contracts + + + +
38
+

90% active rate

+
+
+ + + Verified + + + +
35
+

83% verification rate

+
+
+ + + Pending Updates + + + +
3
+

Require attention

+
+
+
+ + {/* Contracts List */} + + + Contract Registry + + View and manage your smart contract deployments + + + +
+ +

Smart contract management interface coming soon

+

Deploy, verify, and interact with smart contracts across multiple chains

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/databases/page.tsx b/app/d/databases/page.tsx new file mode 100644 index 0000000..e2aa28c --- /dev/null +++ b/app/d/databases/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Database, Plus, Activity, Users, HardDrive } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function DatabasesPage() { + return ( +
+
+
+

Databases

+

+ Manage your database instances and connections +

+
+ +
+ + {/* Stats Cards */} +
+ + + Total Databases + + + +
5
+

3 PostgreSQL, 2 MongoDB

+
+
+ + + Active Connections + + + +
245
+

+15% from last hour

+
+
+ + + Connected Apps + + + +
12
+

Across 3 environments

+
+
+ + + Storage Used + + + +
1.2TB
+

65% of total capacity

+
+
+
+ + {/* Database List */} + + + Database Instances + + Manage your database connections and monitoring + + + +
+ +

Database management interface coming soon

+

Connect your databases to view real-time metrics and manage connections

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/functions/page.tsx b/app/d/functions/page.tsx new file mode 100644 index 0000000..ac9c559 --- /dev/null +++ b/app/d/functions/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Zap, Plus, Activity, Clock, TrendingUp } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function FunctionsPage() { + return ( +
+
+
+

Cloud Functions

+

+ Manage your serverless functions and microservices +

+
+ +
+ + {/* Stats Cards */} +
+ + + Functions + + + +
23
+

18 active, 5 inactive

+
+
+ + + Executions Today + + + +
1,234
+

+20% from yesterday

+
+
+ + + Avg Response Time + + + +
124ms
+

-12ms from last week

+
+
+ + + Success Rate + + + +
99.8%
+

+0.2% this month

+
+
+
+ + {/* Functions List */} + + + Function Deployments + + Monitor and manage your serverless function deployments + + + +
+ +

Function management interface coming soon

+

Deploy and monitor serverless functions with real-time metrics

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/layout.tsx b/app/d/layout.tsx new file mode 100644 index 0000000..6b28179 --- /dev/null +++ b/app/d/layout.tsx @@ -0,0 +1,3 @@ +export default function SmartObjectsLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/app/d/page.tsx b/app/d/page.tsx new file mode 100644 index 0000000..c38da0b --- /dev/null +++ b/app/d/page.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import Link from 'next/link'; +import { + Database, + Zap, + Link as LinkIcon, + FileCode2, + Globe, + Settings, + ArrowRight, + Activity, + TrendingUp, + Users, +} from 'lucide-react'; + +const smartObjects = [ + { + id: 'databases', + title: 'Databases', + description: 'Manage your databases and data storage', + icon: Database, + href: '/d/databases', + color: 'text-blue-600', + status: 'Available', + }, + { + id: 'functions', + title: 'Cloud Functions', + description: 'Serverless functions and microservices', + icon: Zap, + href: '/d/functions', + color: 'text-yellow-600', + status: 'Available', + }, + { + id: 'chains', + title: 'Blockchains', + description: 'Connected blockchain networks', + icon: LinkIcon, + href: '/d/chains', + color: 'text-purple-600', + status: 'Available', + }, + { + id: 'contracts', + title: 'Smart Contracts', + description: 'Deployed smart contracts and ABIs', + icon: FileCode2, + href: '/d/contracts', + color: 'text-green-600', + status: 'Available', + }, + { + id: 'relayers', + title: 'Relayers', + description: 'Cross-chain communication relayers', + icon: Globe, + href: '/d/relayers', + color: 'text-indigo-600', + status: 'Available', + }, + { + id: 'registry', + title: 'Registry', + description: 'Service and contract registry', + icon: Database, + href: '/d/registry', + color: 'text-pink-600', + status: 'Available', + }, +]; + +export default function SmartObjectsDashboard() { + return ( +
+
+

Smart Objects Dashboard

+

+ High-level management interface for your distributed applications, databases, and blockchain infrastructure. +

+
+ {/* Quick Stats */} +
+ + + Active Services + + + +
12
+

+2 from last hour

+
+
+ + + Database Connections + + + +
245
+

+15% from last week

+
+
+ + + Function Executions + + + +
1,234
+

+20% from yesterday

+
+
+ + + Connected Chains + + + +
8
+

All networks healthy

+
+
+
+ {/* Smart Objects Grid */} +
+

Cloud Services

+
+ {smartObjects.slice(0, 2).map((object) => { + const Icon = object.icon; + return ( + + + +
+ +
+
+ {object.title} + {object.description} +
+ +
+
+ + ); + })} +
+
+ {/* Blockchain Section */} +
+

Blockchain Infrastructure

+
+ {smartObjects.slice(2).map((object) => { + const Icon = object.icon; + return ( + + + +
+ +
+
+ {object.title} + {object.description} +
+ +
+
+ + ); + })} +
+
+ {/* Quick Actions */} +
+

Quick Actions

+
+ + + System Health + + +

+ All systems operational. View detailed metrics in individual service pages. +

+
+
+ + + Recent Activity + + +

+ Latest deployments, function executions, and blockchain transactions. +

+
+
+
+
+
+ ); +} diff --git a/app/d/playground/assets/page.tsx b/app/d/playground/assets/page.tsx new file mode 100644 index 0000000..8f9428e --- /dev/null +++ b/app/d/playground/assets/page.tsx @@ -0,0 +1,7 @@ +import { AssetsRoute } from '@/components/routes/assets.route'; + +function AssetsPage() { + return ; +} + +export default AssetsPage; diff --git a/app/d/playground/contracts/page.tsx b/app/d/playground/contracts/page.tsx new file mode 100644 index 0000000..5b9c2fc --- /dev/null +++ b/app/d/playground/contracts/page.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; + +import { useContracts } from '@/hooks/contract/use-contracts'; +import { usePagination } from '@/hooks/use-pagination'; +import { shortenAddress } from '@/lib/chain'; +import { Button } from '@/components/ui/button'; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Spinner } from '@/components/common/spinner'; +import { routes } from '@/routes'; + +const ITEMS_PER_PAGE = 10; +const INITIAL_PAGE = 1; + +function Contracts() { + const [currentPage, setCurrentPage] = useState(INITIAL_PAGE); + + const { data, isLoading } = useContracts(currentPage, ITEMS_PER_PAGE); + const contracts = data?.contracts || []; + const pagination = data?.pagination; + const totalItems = pagination?.total || 0; + + const { pageNumbers, totalPages, goToPage, goToPreviousPage, goToNextPage, paginationSummary } = usePagination({ + totalItems, + currentPage, + itemsPerPage: ITEMS_PER_PAGE, + onPageChange: setCurrentPage, + }); + + return ( +
+

Contracts

+
+ {isLoading ? ( + + ) : contracts.length === 0 ? ( +
+ No contracts deployed yet +
+ ) : ( + + + + Contract Index + Contract Address + Creator + + + + + {contracts.map(({ index, creator, contractAddress }) => ( + + {index.toString()} + {shortenAddress(contractAddress, 8)} + {shortenAddress(creator)} + +
+ + + + + + +
+
+
+ ))} +
+
+ )} +
+ {!isLoading && contracts.length > 0 && ( +
+

{paginationSummary}

+
+ {totalPages > 1 && ( + + + + {pageNumbers.map((pageNumber, index) => + pageNumber < 0 ? ( + + ) : ( + goToPage(pageNumber)} + > + {pageNumber} + + ) + )} + + + + )} +
+
+
+ )} +
+ ); +} + +export default Contracts; diff --git a/app/d/playground/create/page.tsx b/app/d/playground/create/page.tsx new file mode 100644 index 0000000..b94d593 --- /dev/null +++ b/app/d/playground/create/page.tsx @@ -0,0 +1,34 @@ +import { join } from 'path'; + +import { + FALLBACK_PROJECT_PATH, + getFallbackProjectItems, +} from '@/components/contract/editor/contract-editor.server-utils'; +import { ContractPanes } from '@/components/contract/editor/contract-panes'; +import { type ProjectItem } from '@/components/contract/editor/hooks/use-contract-project'; + +interface CreateContractProps { + fallbackProjectItems: Record; +} + +// Server component to handle the data fetching +async function getFallbackData(): Promise> { + // Read fallback project files + const fallbackPath = join(process.cwd(), FALLBACK_PROJECT_PATH); + const fallbackProjectItems = getFallbackProjectItems(fallbackPath, fallbackPath); + return fallbackProjectItems; +} + +function CreateContractPage({ fallbackProjectItems }: CreateContractProps) { + return ( +
+ +
+ ); +} + +export default async function CreatePage() { + const fallbackProjectItems = await getFallbackData(); + + return ; +} diff --git a/app/d/playground/deploy/page.tsx b/app/d/playground/deploy/page.tsx new file mode 100644 index 0000000..c0af5c3 --- /dev/null +++ b/app/d/playground/deploy/page.tsx @@ -0,0 +1,331 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { DeliverTxResponse } from 'hyperwebjs/types'; +import { useQueryState } from 'nuqs'; +import { useForm } from 'react-hook-form'; +import { GoGear } from 'react-icons/go'; +import { z } from 'zod'; + +import { useContracts } from '@/hooks/contract/use-contracts'; +import { useHyperwebChain } from '@/hooks/contract/use-hyperweb-chain'; +import { useInstantiateTx } from '@/hooks/contract/use-instantiate-tx'; +import { useBreakpoint } from '@/hooks/use-breakpoint'; +import { getInstantiateResponse, readFileContent } from '@/lib/contract/deploy'; +import { logger } from '@/lib/logger'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Combobox } from '@/components/contract/combobox'; +import { DeployResult } from '@/components/contract/deploy/deploy-result'; +import { FileUploader } from '@/components/contract/deploy/file-uploader'; +import { getOutputJsContracts } from '@/components/contract/editor/contract-editor.utils'; +import { useContractProject } from '@/components/contract/editor/hooks/use-contract-project'; +import { contractSourceStoreActions } from '@/contexts/contract-source'; +import { useSavedContracts } from '@/contexts/saved-contracts'; + +const formSchema = z.object({ + contractSource: z + .discriminatedUnion('type', [ + z.object({ + type: z.literal('select'), + contractId: z.string().min(1), + }), + z.object({ + type: z.literal('upload'), + file: z.instanceof(File).optional(), + sourceFile: z.instanceof(File).optional(), + }), + ]) + .refine( + (data) => { + if (data.type === 'upload') { + return data.file instanceof File; + } + return true; + }, + { + message: 'Contract file is required for upload', + } + ), + contractLabel: z.string().optional(), +}); + +type FormSchema = z.infer; + +function Deploy() { + const [txResult, setTxResult] = useState(); + const items = useContractProject((state) => state.items); + const getItemById = useContractProject((state) => state.getItemById); + const { saveContract } = useSavedContracts(); + const { isBelowSm } = useBreakpoint('sm'); + + const outputContracts = useMemo(() => getOutputJsContracts(items), [items]); + + const [contractId] = useQueryState('contractId'); + + const { instantiateTx } = useInstantiateTx(); + const { address, connect } = useHyperwebChain(); + const { refetch: updateContracts } = useContracts(); + + const form = useForm({ + resolver: zodResolver(formSchema), + mode: 'onChange', + defaultValues: { + contractSource: { + type: 'select', + contractId: '', + }, + contractLabel: '', + }, + }); + + const { + formState: { isValid, isSubmitting }, + setValue: setFormValue, + } = form; + + const updateLabelValue = useCallback( + (name: string) => { + const labelValue = name.replace(/\.js$/, ''); + setFormValue('contractLabel', labelValue); + }, + [setFormValue] + ); + + useEffect(() => { + if (contractId) { + const selectedContract = getItemById(contractId, 'file'); + + if (selectedContract && selectedContract.path.startsWith('dist/') && selectedContract.path.endsWith('.js')) { + setFormValue( + 'contractSource', + { + type: 'select', + contractId: selectedContract.id, + }, + { shouldValidate: true } + ); + + updateLabelValue(selectedContract.path.split('/').pop() || selectedContract.path); + } + } + }, [contractId, updateLabelValue]); + + async function onSubmit(values: FormSchema) { + let code: string; + let source: string | undefined = undefined; + const { contractSource, contractLabel } = values; + + if (contractSource.type === 'upload') { + if (!contractSource.file) { + throw new Error('Contract file is required for upload'); + } + code = await readFileContent(contractSource.file); + // Source file is optional for upload + if (contractSource.sourceFile) { + source = await readFileContent(contractSource.sourceFile); + } + } else { + const selectedContract = getItemById(contractSource.contractId, 'file'); + code = selectedContract?.content ?? ''; + + // Get source files from store using the contract's path + if (selectedContract) { + const sourceFiles = contractSourceStoreActions.getSourceFiles(selectedContract.path); + if (sourceFiles && Object.keys(sourceFiles).length > 0) { + source = JSON.stringify(sourceFiles, null, 2); + } + } + } + + await instantiateTx({ + address, + code, + source, + onTxSucceed: (txInfo) => { + if (txInfo.code !== 0) { + throw new Error(txInfo.rawLog); + } + setTxResult(txInfo); + updateContracts(); + + const { data: instantiateResponse, error } = getInstantiateResponse(txInfo); + + if (error) { + logger.error('Failed to parse instantiate response:', error); + } + + const { index, address: contractAddress } = instantiateResponse || { index: 0, address: '' }; + saveContract(address, index.toString(), contractAddress, contractLabel ?? ''); + }, + }); + } + + if (txResult) { + return ( + { + setTxResult(undefined); + form.reset(); + }} + /> + ); + } + + return ( +
+

Deploy Contract

+ +
+ + { + return ( + + Contract File + + { + field.onChange({ + type: value, + }); + + setFormValue('contractLabel', ''); + }} + > + + {isBelowSm ? 'Select' : 'Select from your contracts'} + {isBelowSm ? 'Upload' : 'Upload JS contract'} + + + + Select Contract + + ({ + value: contract.id, + label: contract.path.split('/').pop() || contract.path, + }))} + value={field.value.type === 'select' ? field.value.contractId : ''} + displayMode="label" + onChange={(value) => { + // When a contract is selected, update the label + const selectedContract = getItemById(value, 'file'); + + field.onChange({ + type: 'select', + contractId: value, + }); + + if (selectedContract) { + updateLabelValue(selectedContract.path.split('/').pop() || selectedContract.path); + } + }} + placeholder="Select contract" + /> + + Select a contract from your compiled contracts. + + + +
+ Contract File + + { + if (file) { + field.onChange({ + type: 'upload', + file, + sourceFile: field.value.type === 'upload' ? field.value.sourceFile : undefined, + }); + updateLabelValue(file.name); + } else { + // Handle file removal + field.onChange({ + type: 'upload', + file: undefined, + sourceFile: field.value.type === 'upload' ? field.value.sourceFile : undefined, + }); + } + }} + /> + +
+ +
+ Source File (Optional) + + { + if (file) { + field.onChange({ + type: 'upload', + file: field.value.type === 'upload' ? field.value.file : undefined, + sourceFile: file, + }); + } else { + // Handle file removal + field.onChange({ + type: 'upload', + file: field.value.type === 'upload' ? field.value.file : undefined, + sourceFile: undefined, + }); + } + }} + /> + + + Upload a JSON file that maps contract TypeScript filenames to their source code content. + +
+
+
+
+ ); + }} + /> + + ( + + Label (Optional) + + + + + Add a brief label to describe what the contract does. The label will be stored locally. + + + )} + /> + + {address ? ( + + ) : ( + + )} + + +
+ ); +} + +export default Deploy; diff --git a/app/d/playground/faucet/page.tsx b/app/d/playground/faucet/page.tsx new file mode 100644 index 0000000..aef5d11 --- /dev/null +++ b/app/d/playground/faucet/page.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useMemo } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { useHyperwebChain } from '@/hooks/contract/use-hyperweb-chain'; +import { useHyperwebChainInfo } from '@/hooks/contract/use-hyperweb-chain-info'; +import { getHyperwebConfig } from '@/configs/hyperweb-config'; +import { useToast } from '@/hooks/use-toast'; +import { creditFromFaucet } from '@/lib/contract/faucet'; +import { createBech32AddressSchema } from '@/lib/validations'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +function Faucet() { + const config = getHyperwebConfig(); + + const { data: chainInfo } = useHyperwebChainInfo(); + const { address, assetList } = useHyperwebChain(); + const { toast } = useToast(); + + const formSchema = useMemo(() => { + return z.object({ + address: createBech32AddressSchema(chainInfo?.chain.bech32Prefix ?? ''), + }); + }, [chainInfo?.chain.bech32Prefix]); + + type FormSchema = z.infer; + + const form = useForm({ + resolver: zodResolver(formSchema), + mode: 'onChange', + defaultValues: { + address: '', + }, + }); + + const { + formState: { isValid, isSubmitting }, + } = form; + + async function onSubmit(values: FormSchema) { + const asset = assetList.assets[0]; + const faucetEndpoint = config?.chain.faucet; + const faucetUrl = `${faucetEndpoint}/credit`; + + try { + if (!faucetEndpoint) { + throw new Error('Faucet endpoint is not set'); + } + await creditFromFaucet(values.address, asset.base, faucetUrl); + toast({ + variant: 'success', + title: 'Tokens credited', + duration: 3000, + }); + } catch (error: any) { + console.error(error); + toast({ + variant: 'destructive', + title: 'Failed to get tokens', + description: error.message, + duration: 5000, + }); + } + } + + return ( +
+
+

Faucet

+

Get some testnet tokens to your account

+
+ +
+ + { + return ( + + Wallet Address +
+ + + + +
+ +
+ ); + }} + /> + + + +
+ ); +} + +export default Faucet; diff --git a/app/d/playground/interact/page.tsx b/app/d/playground/interact/page.tsx new file mode 100644 index 0000000..64f5b92 --- /dev/null +++ b/app/d/playground/interact/page.tsx @@ -0,0 +1,7 @@ +import { InteractRoute } from '@/components/routes/interact.route'; + +function InteractPage() { + return ; +} + +export default InteractPage; diff --git a/app/d/playground/layout.tsx b/app/d/playground/layout.tsx new file mode 100644 index 0000000..86cfbdc --- /dev/null +++ b/app/d/playground/layout.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { ContractLayoutProvider } from '@/components/contract/contract-layout'; + +export default function PlaygroundLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/app/d/playground/page.tsx b/app/d/playground/page.tsx new file mode 100644 index 0000000..f7cc3c1 --- /dev/null +++ b/app/d/playground/page.tsx @@ -0,0 +1,108 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { + Coins, // Faucet + DatabaseZap, // Query + FilePlus, // Create + FileText, // Contracts + Play, // Execute + Rocket, // Deploy +} from 'lucide-react'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +const features = [ + { + title: 'Create Contract', + description: 'Start building contracts from scratch or templates.', + href: '/d/playground/create', + icon: FilePlus, + bgColor: 'bg-blue-50 dark:bg-blue-900/30', + iconColor: 'text-blue-600 dark:text-blue-400', + }, + { + title: 'Explore Contracts', + description: 'Browse and interact with contracts created by the community.', + href: '/d/playground/contracts', + icon: FileText, + bgColor: 'bg-purple-50 dark:bg-purple-900/30', + iconColor: 'text-purple-600 dark:text-purple-400', + }, + { + title: 'Deploy Contract', + description: 'Deploy your compiled contracts to the selected network.', + href: '/d/playground/deploy', + icon: Rocket, + bgColor: 'bg-green-50 dark:bg-green-900/30', + iconColor: 'text-green-600 dark:text-green-400', + }, + { + title: 'Execute Message', + description: 'Interact with contracts by sending execute messages.', + href: '/d/playground/interact?tab=execute', + icon: Play, + bgColor: 'bg-yellow-50 dark:bg-yellow-900/30', + iconColor: 'text-yellow-600 dark:text-yellow-400', + }, + { + title: 'Query State', + description: 'Read current data and state directly from contracts.', + href: '/d/playground/interact?tab=query', + icon: DatabaseZap, + bgColor: 'bg-indigo-50 dark:bg-indigo-900/30', + iconColor: 'text-indigo-600 dark:text-indigo-400', + }, + { + title: 'Faucet', + description: 'Get testnet tokens needed for deployment and execution.', + href: '/d/playground/faucet', + icon: Coins, + bgColor: 'bg-pink-50 dark:bg-pink-900/30', + iconColor: 'text-pink-600 dark:text-pink-400', + }, +]; + +function ContractPlaygroundIndex() { + return ( +
+

+ Explore the Playground +

+ +
+
+ {features.map((feature) => ( + + + +
+
+ {feature.title} +
+ + {feature.description} + +
+ + ))} +
+
+
+ ); +} + +export default ContractPlaygroundIndex; diff --git a/app/d/registry/page.tsx b/app/d/registry/page.tsx new file mode 100644 index 0000000..1c6f8ce --- /dev/null +++ b/app/d/registry/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Database, Plus, Search, Tag, FileCode2 } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function RegistryPage() { + return ( +
+
+
+

Service Registry

+

+ Central registry for services, contracts, and APIs +

+
+ +
+ + {/* Stats Cards */} +
+ + + Registered Services + + + +
156
+

Across all environments

+
+
+ + + API Endpoints + + + +
342
+

REST, GraphQL, gRPC

+
+
+ + + Tags + + + +
48
+

Organized categories

+
+
+ + + Schemas + + + +
89
+

OpenAPI, GraphQL schemas

+
+
+
+ + {/* Registry List */} + + + Service Directory + + Browse and manage registered services and APIs + + + +
+ +

Service registry interface coming soon

+

Centralized discovery and management for all your services and APIs

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/relayers/page.tsx b/app/d/relayers/page.tsx new file mode 100644 index 0000000..fe2cb0d --- /dev/null +++ b/app/d/relayers/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Globe, Plus, Activity, ArrowRightLeft, Zap } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function RelayersPage() { + return ( +
+
+
+

Cross-Chain Relayers

+

+ Manage cross-chain communication and message relaying +

+
+ +
+ + {/* Stats Cards */} +
+ + + Active Relayers + + + +
6
+

All operational

+
+
+ + + Messages Relayed + + + +
2,847
+

+12% from last week

+
+
+ + + Success Rate + + + +
99.2%
+

Highly reliable

+
+
+ + + Avg Relay Time + + + +
4.2s
+

Cross-chain average

+
+
+
+ + {/* Relayers List */} + + + Relayer Network + + Monitor cross-chain relayer performance and status + + + +
+ +

Cross-chain relayer management interface coming soon

+

Set up and monitor relayers for seamless cross-chain communication

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/d/settings/page.tsx b/app/d/settings/page.tsx new file mode 100644 index 0000000..0e8d0a2 --- /dev/null +++ b/app/d/settings/page.tsx @@ -0,0 +1,89 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Settings, User, Key, Bell, Shield } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export default function SettingsPage() { + return ( +
+
+

Settings

+

+ Configure your Smart Objects dashboard preferences and integrations +

+
+ + {/* Settings Sections */} +
+ + + + + Profile Settings + + + Manage your user profile and preferences + + + +
+

Profile management interface coming soon

+
+
+
+ + + + + + API Keys & Integrations + + + Manage API keys and external service integrations + + + +
+

API key management interface coming soon

+
+
+
+ + + + + + Notifications + + + Configure alerts and notification preferences + + + +
+

Notification settings interface coming soon

+
+
+
+ + + + + + Security + + + Security settings and access control + + + +
+

Security settings interface coming soon

+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/databases/page.tsx b/app/databases/page.tsx new file mode 100644 index 0000000..b282a24 --- /dev/null +++ b/app/databases/page.tsx @@ -0,0 +1,38 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Database } from 'lucide-react' + +export default function DatabasesPage() { + return ( +
+
+

Databases

+

+ Manage your cloud databases and data services. +

+
+ +
+ + +
+ +
+
+ Cloud Databases + + High-level database abstractions + +
+
+ +

+ Coming soon - manage your databases without infrastructure complexity +

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/editor/page.tsx b/app/editor/page.tsx new file mode 100644 index 0000000..7637319 --- /dev/null +++ b/app/editor/page.tsx @@ -0,0 +1,243 @@ +'use client' + +import { useState, useEffect } from 'react' +import dynamic from 'next/dynamic' +import { Button } from '@/components/ui/button' +import { EnhancedFileExplorer } from '@/components/ide/enhanced-file-explorer' +import { Terminal } from '@/components/ide/terminal' +import { fs } from '@zenfs/core' +import { + Play, + GitBranch, + GitCommit, + Upload, + Download, + Save, + Terminal as TerminalIcon, + Code, + RefreshCw, + Check, + X +} from 'lucide-react' + +// Dynamic imports to avoid SSR issues +const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { ssr: false }) + +export default function IDEPage() { + const [activeFile, setActiveFile] = useState(null) + const [fileContent, setFileContent] = useState('') + const [terminalVisible, setTerminalVisible] = useState(false) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const [fileLanguage, setFileLanguage] = useState('javascript') + const [fileSource, setFileSource] = useState<'local' | 'zenfs'>('local') + const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle') + const [modifiedFiles, setModifiedFiles] = useState>(new Set()) + + // Handle file selection from FileExplorer + const handleFileSelect = (path: string, content: string, source: 'local' | 'zenfs') => { + setActiveFile(path) + setFileContent(content) + setFileSource(source) + setHasUnsavedChanges(false) + + // Detect language from file extension + const ext = path.split('.').pop()?.toLowerCase() + switch (ext) { + case 'js': + case 'jsx': + setFileLanguage('javascript') + break + case 'ts': + case 'tsx': + setFileLanguage('typescript') + break + case 'html': + setFileLanguage('html') + break + case 'css': + setFileLanguage('css') + break + case 'json': + setFileLanguage('json') + break + case 'md': + setFileLanguage('markdown') + break + case 'yaml': + case 'yml': + setFileLanguage('yaml') + break + default: + setFileLanguage('plaintext') + } + } + + // Handle file content changes + const handleContentChange = (value: string | undefined) => { + setFileContent(value || '') + setHasUnsavedChanges(true) + } + + // Save file to ZenFS + const saveFile = async () => { + if (activeFile && hasUnsavedChanges) { + try { + await fs.promises.writeFile(`/project/${activeFile}`, fileContent) + setHasUnsavedChanges(false) + setFileSource('zenfs') + // Track as modified for sync + setModifiedFiles(prev => new Set(prev).add(activeFile)) + } catch (error) { + console.error('Failed to save file:', error) + } + } + } + + // Sync files from ZenFS to local file system + const syncFiles = async () => { + setSyncStatus('syncing') + + try { + let successCount = 0 + let errorCount = 0 + + for (const filePath of Array.from(modifiedFiles)) { + try { + // Read content from ZenFS + const content = await fs.promises.readFile(`/project/${filePath}`, 'utf8') + + // Write to local file system + const response = await fetch('/api/ide/fs/write', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filePath, content }) + }) + + if (response.ok) { + successCount++ + } else { + errorCount++ + console.error(`Failed to sync ${filePath}`) + } + } catch (error) { + errorCount++ + console.error(`Error syncing ${filePath}:`, error) + } + } + + if (errorCount === 0) { + setSyncStatus('success') + setModifiedFiles(new Set()) + setTimeout(() => setSyncStatus('idle'), 3000) + } else { + setSyncStatus('error') + setTimeout(() => setSyncStatus('idle'), 3000) + } + } catch (error) { + console.error('Sync failed:', error) + setSyncStatus('error') + setTimeout(() => setSyncStatus('idle'), 3000) + } + } + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault() + saveFile() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [activeFile, fileContent]) + + return ( +
+ {/* Main Content Area */} +
+ {/* File Explorer (Left Sidebar) */} +
+ +
+ + {/* Editor and Terminal Container */} +
+ {/* Code Editor */} +
+ {activeFile ? ( + + ) : ( +
+
+ +

Select a file to start editing

+
+
+ )} +
+ + {/* Terminal */} + {terminalVisible && ( +
+
+
+ + Terminal +
+ +
+
+ +
+
+ )} +
+
+ + {/* Status Bar */} +
+
+ UTF-8 + {fileLanguage} + Ln 1, Col 1 + + {fileSource === 'zenfs' ? ( + <>ZenFS + ) : ( + <>Local + )} + +
+
+ + {hasUnsavedChanges ? 'Modified' : 'Ready'} +
+
+
+ ) +} \ No newline at end of file diff --git a/app/functions/page.tsx b/app/functions/page.tsx new file mode 100644 index 0000000..4889631 --- /dev/null +++ b/app/functions/page.tsx @@ -0,0 +1,38 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Zap } from 'lucide-react' + +export default function FunctionsPage() { + return ( +
+
+

Cloud Functions

+

+ Deploy and manage serverless functions. +

+
+ +
+ + +
+ +
+
+ Serverless Functions + + Auto-scaling compute functions + +
+
+ +

+ Coming soon - deploy functions without managing infrastructure +

+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index b219a46..8abdb15 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,71 +1,71 @@ @tailwind base; @tailwind components; @tailwind utilities; - + @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; - + --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; - + --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - + --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; - + --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; - + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - + --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - + --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; - + --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - + --radius: 0.5rem; } - + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; - + --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; - + --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - + --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; - + --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; - + --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - + --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; - + --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - + --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } - + @layer base { * { @apply border-border; @@ -73,4 +73,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} diff --git a/app/all/page.tsx b/app/i/all/page.tsx similarity index 100% rename from app/all/page.tsx rename to app/i/all/page.tsx diff --git a/app/configmaps/page.tsx b/app/i/configmaps/page.tsx similarity index 100% rename from app/configmaps/page.tsx rename to app/i/configmaps/page.tsx diff --git a/app/cronjobs/page.tsx b/app/i/cronjobs/page.tsx similarity index 100% rename from app/cronjobs/page.tsx rename to app/i/cronjobs/page.tsx diff --git a/app/daemonsets/page.tsx b/app/i/daemonsets/page.tsx similarity index 100% rename from app/daemonsets/page.tsx rename to app/i/daemonsets/page.tsx diff --git a/app/deployments/page.tsx b/app/i/deployments/page.tsx similarity index 100% rename from app/deployments/page.tsx rename to app/i/deployments/page.tsx diff --git a/app/endpoints/page.tsx b/app/i/endpoints/page.tsx similarity index 100% rename from app/endpoints/page.tsx rename to app/i/endpoints/page.tsx diff --git a/app/endpointslices/page.tsx b/app/i/endpointslices/page.tsx similarity index 100% rename from app/endpointslices/page.tsx rename to app/i/endpointslices/page.tsx diff --git a/app/events/page.tsx b/app/i/events/page.tsx similarity index 100% rename from app/events/page.tsx rename to app/i/events/page.tsx diff --git a/app/hpas/page.tsx b/app/i/hpas/page.tsx similarity index 100% rename from app/hpas/page.tsx rename to app/i/hpas/page.tsx diff --git a/app/ingresses/page.tsx b/app/i/ingresses/page.tsx similarity index 100% rename from app/ingresses/page.tsx rename to app/i/ingresses/page.tsx diff --git a/app/jobs/page.tsx b/app/i/jobs/page.tsx similarity index 100% rename from app/jobs/page.tsx rename to app/i/jobs/page.tsx diff --git a/app/networkpolicies/page.tsx b/app/i/networkpolicies/page.tsx similarity index 100% rename from app/networkpolicies/page.tsx rename to app/i/networkpolicies/page.tsx diff --git a/app/i/page.tsx b/app/i/page.tsx new file mode 100644 index 0000000..500f36f --- /dev/null +++ b/app/i/page.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Suspense } from 'react'; +import Link from 'next/link'; +import { Package, Server, Key, Settings, FileCode2, Copy, Activity, ArrowRight, Layers } from 'lucide-react'; + +const resources = [ + { + id: 'all', + title: 'View All', + description: 'See all resources in one dashboard', + icon: Layers, + href: '/i/all', + color: 'text-gray-600', + }, + { + id: 'deployments', + title: 'Deployments', + description: 'Manage and monitor your deployments', + icon: Package, + href: '/i/deployments', + color: 'text-blue-600', + }, + { + id: 'services', + title: 'Services', + description: 'Manage your services and networking', + icon: Server, + href: '/i/services', + color: 'text-green-600', + }, + { + id: 'secrets', + title: 'Secrets', + description: 'Manage sensitive data and credentials', + icon: Key, + href: '/i/secrets', + color: 'text-yellow-600', + }, + { + id: 'configmaps', + title: 'ConfigMaps', + description: 'Manage application configuration data', + icon: Settings, + href: '/i/configmaps', + color: 'text-purple-600', + }, + { + id: 'templates', + title: 'Templates', + description: 'Manage and deploy resource templates', + icon: FileCode2, + href: '/i/templates', + color: 'text-indigo-600', + }, + { + id: 'replicasets', + title: 'ReplicaSets', + description: 'Manage and monitor your replica sets', + icon: Copy, + href: '/i/replicasets', + color: 'text-pink-600', + }, + { + id: 'pods', + title: 'Pods', + description: 'Monitor and manage individual pods', + icon: Activity, + href: '/i/pods', + color: 'text-orange-600', + }, +]; + +export default function InfrastructureOverviewPage() { + return ( +
+
+

Infrastructure Overview

+

+ Manage your Kubernetes infrastructure resources. Select a resource type below to get started. +

+
+
+ {resources.map((resource) => { + const Icon = resource.icon; + return ( + + + +
+ +
+
+ {resource.title} + {resource.description} +
+ +
+
+ + ); + })} +
+
+ + + Quick Stats + + +

+ View detailed statistics for each resource type by navigating to their respective pages. +

+
+
+ + + Namespace + + +

+ Use the namespace switcher in the header to change the active namespace. +

+
+
+ + + API Connection + + +

+ Connected to Kubernetes API. Ensure kubectl proxy is running on port 8001. +

+
+
+
+
+ ); +} diff --git a/app/pdbs/page.tsx b/app/i/pdbs/page.tsx similarity index 100% rename from app/pdbs/page.tsx rename to app/i/pdbs/page.tsx diff --git a/app/pods/page.tsx b/app/i/pods/page.tsx similarity index 100% rename from app/pods/page.tsx rename to app/i/pods/page.tsx diff --git a/app/priorityclasses/page.tsx b/app/i/priorityclasses/page.tsx similarity index 100% rename from app/priorityclasses/page.tsx rename to app/i/priorityclasses/page.tsx diff --git a/app/pvcs/page.tsx b/app/i/pvcs/page.tsx similarity index 100% rename from app/pvcs/page.tsx rename to app/i/pvcs/page.tsx diff --git a/app/pvs/page.tsx b/app/i/pvs/page.tsx similarity index 100% rename from app/pvs/page.tsx rename to app/i/pvs/page.tsx diff --git a/app/replicasets/page.tsx b/app/i/replicasets/page.tsx similarity index 100% rename from app/replicasets/page.tsx rename to app/i/replicasets/page.tsx diff --git a/app/resourcequotas/page.tsx b/app/i/resourcequotas/page.tsx similarity index 100% rename from app/resourcequotas/page.tsx rename to app/i/resourcequotas/page.tsx diff --git a/app/rolebindings/page.tsx b/app/i/rolebindings/page.tsx similarity index 100% rename from app/rolebindings/page.tsx rename to app/i/rolebindings/page.tsx diff --git a/app/roles/page.tsx b/app/i/roles/page.tsx similarity index 100% rename from app/roles/page.tsx rename to app/i/roles/page.tsx diff --git a/app/runtimeclasses/page.tsx b/app/i/runtimeclasses/page.tsx similarity index 100% rename from app/runtimeclasses/page.tsx rename to app/i/runtimeclasses/page.tsx diff --git a/app/secrets/page.tsx b/app/i/secrets/page.tsx similarity index 100% rename from app/secrets/page.tsx rename to app/i/secrets/page.tsx diff --git a/app/serviceaccounts/page.tsx b/app/i/serviceaccounts/page.tsx similarity index 100% rename from app/serviceaccounts/page.tsx rename to app/i/serviceaccounts/page.tsx diff --git a/app/services/page.tsx b/app/i/services/page.tsx similarity index 100% rename from app/services/page.tsx rename to app/i/services/page.tsx diff --git a/app/statefulsets/page.tsx b/app/i/statefulsets/page.tsx similarity index 100% rename from app/statefulsets/page.tsx rename to app/i/statefulsets/page.tsx diff --git a/app/storageclasses/page.tsx b/app/i/storageclasses/page.tsx similarity index 100% rename from app/storageclasses/page.tsx rename to app/i/storageclasses/page.tsx diff --git a/app/templates/page.tsx b/app/i/templates/page.tsx similarity index 100% rename from app/templates/page.tsx rename to app/i/templates/page.tsx diff --git a/app/volumeattachments/page.tsx b/app/i/volumeattachments/page.tsx similarity index 100% rename from app/volumeattachments/page.tsx rename to app/i/volumeattachments/page.tsx diff --git a/app/layout.tsx b/app/layout.tsx index eb0a6ef..f6db619 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,30 +1,28 @@ -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -import './globals.css' -import { Providers } from './providers' -import { DashboardLayout } from '@/components/dashboard-layout' +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; +import '@interchain-ui/react/styles'; +import { Providers } from './providers'; +import { AppLayout } from '@/components/app-layout'; +import { NuqsAdapter } from 'nuqs/adapters/next/app'; -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { title: 'Kubernetes Dashboard', description: 'A modern dashboard for managing Kubernetes resources', -} +}; -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + - - - {children} - - + + + {children} + + - ) -} \ No newline at end of file + ); +} diff --git a/app/page.tsx b/app/page.tsx index c8683b2..25c0dc9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,154 +1,14 @@ 'use client' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import Link from 'next/link' -import { - Package, - Server, - Key, - Settings, - FileCode2, - Copy, - Activity, - ArrowRight, - Layers -} from 'lucide-react' - -const resources = [ - - { - id: 'all', - title: 'View All', - description: 'See all resources in one dashboard', - icon: Layers, - href: '/all', - color: 'text-gray-600' - }, - { - id: 'deployments', - title: 'Deployments', - description: 'Manage and monitor your deployments', - icon: Package, - href: '/deployments', - color: 'text-blue-600' - }, - { - id: 'services', - title: 'Services', - description: 'Manage your services and networking', - icon: Server, - href: '/services', - color: 'text-green-600' - }, - { - id: 'secrets', - title: 'Secrets', - description: 'Manage sensitive data and credentials', - icon: Key, - href: '/secrets', - color: 'text-yellow-600' - }, - { - id: 'configmaps', - title: 'ConfigMaps', - description: 'Manage application configuration data', - icon: Settings, - href: '/configmaps', - color: 'text-purple-600' - }, - { - id: 'templates', - title: 'Templates', - description: 'Manage and deploy resource templates', - icon: FileCode2, - href: '/templates', - color: 'text-indigo-600' - }, - { - id: 'replicasets', - title: 'ReplicaSets', - description: 'Manage and monitor your replica sets', - icon: Copy, - href: '/replicasets', - color: 'text-pink-600' - }, - { - id: 'pods', - title: 'Pods', - description: 'Monitor and manage individual pods', - icon: Activity, - href: '/pods', - color: 'text-orange-600' - } -] - -export default function OverviewPage() { - return ( -
-
-

Welcome to Kubernetes Dashboard

-

- This dashboard provides a user-friendly interface for managing your Kubernetes resources. - Select a resource type below to get started. -

-
- -
- {resources.map((resource) => { - const Icon = resource.icon - return ( - - - -
- -
-
- {resource.title} - - {resource.description} - -
- -
-
- - ) - })} -
- -
- - - Quick Stats - - -

- View detailed statistics for each resource type by navigating to their respective pages. -

-
-
- - - Namespace - - -

- Use the namespace switcher in the header to change the active namespace. -

-
-
- - - API Connection - - -

- Connected to Kubernetes API. Ensure kubectl proxy is running on port 8001. -

-
-
-
-
- ) +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' + +export default function HomePage() { + const router = useRouter() + + useEffect(() => { + router.replace('/d') + }, [router]) + + return null } \ No newline at end of file diff --git a/app/providers.tsx b/app/providers.tsx index 6d590a9..4a16169 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,23 +1,29 @@ -'use client' +'use client'; -import { KubernetesProvider } from '../k8s/context' -import { NamespaceProvider } from '@/contexts/NamespaceContext' -import { ConfirmProvider } from '@/hooks/useConfirm' +import { ThemeProvider } from 'next-themes'; +import { KubernetesProvider } from '../k8s/context'; +import { NamespaceProvider } from '@/contexts/NamespaceContext'; +import { AppProvider } from '@/contexts/AppContext'; +import { ConfirmProvider } from '@/hooks/useConfirm'; interface ProvidersProps { - children: React.ReactNode + children: React.ReactNode; } export function Providers({ children }: ProvidersProps) { return ( - - - - {children} - - - - ) -} \ No newline at end of file + + + + + {children} + + + + + ); +} diff --git a/components/adaptive-layout.tsx b/components/adaptive-layout.tsx new file mode 100644 index 0000000..ca7f93a --- /dev/null +++ b/components/adaptive-layout.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import { DashboardLayout } from './dashboard-layout'; +import { IDELayout } from './ide-layout'; + +interface AdaptiveLayoutProps { + children: React.ReactNode; + onChatToggle: () => void; + chatVisible: boolean; + chatLayoutMode: 'floating' | 'snapped'; + chatWidth: number; + // IDE specific props (passed through when in editor mode) + ideProps?: { + activeFile?: string | null; + hasUnsavedChanges?: boolean; + onSaveFile?: () => void; + onSyncFiles?: () => void; + syncStatus?: 'idle' | 'syncing' | 'success' | 'error'; + modifiedFilesCount?: number; + }; +} + +// Determine layout mode purely from route +function getModeFromRoute(pathname: string): 'smart-objects' | 'infra' | 'editor' { + if (pathname === '/editor') { + return 'editor'; + } + if (pathname === '/d' || pathname.startsWith('/d/')) { + return 'smart-objects'; + } + if (pathname === '/i' || pathname.startsWith('/i/')) { + return 'infra'; + } + // Legacy routes without prefix default to infra + return 'infra'; +} + +export function AdaptiveLayout({ ideProps, ...props }: AdaptiveLayoutProps) { + const pathname = usePathname(); + const mode = getModeFromRoute(pathname); + + // Use IDE layout for /editor route + if (mode === 'editor') { + return ; + } + + // Use dashboard layout with mode for everything else + return ; +} diff --git a/components/agent-manager-agentic.tsx b/components/agent-manager-agentic.tsx new file mode 100644 index 0000000..cd5a4e3 --- /dev/null +++ b/components/agent-manager-agentic.tsx @@ -0,0 +1,566 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Loader2, + Download, + Trash2, + CheckCircle, + AlertCircle, + Server, + Settings, + Plus, + FolderOpen, + Brain, + Sparkles +} from 'lucide-react' +import { AgentKit } from 'agentic-kit' +import OllamaClient from '@agentic-kit/ollama' +import type { AgentProvider } from './ai-chat-agentic' + +interface AgentManagerAgenticProps { + isOpen: boolean + onClose: () => void + agentKit: AgentKit + currentProvider: AgentProvider + onProviderChange: (provider: AgentProvider) => void +} + +export function AgentManagerAgentic({ + isOpen, + onClose, + agentKit, + currentProvider, + onProviderChange +}: AgentManagerAgenticProps) { + // Connection states + const [ollamaStatus, setOllamaStatus] = useState<'checking' | 'online' | 'offline'>('checking') + const [bradieStatus, setBradieStatus] = useState<'checking' | 'online' | 'offline'>('checking') + + // Model management + const [ollamaModels, setOllamaModels] = useState([]) + const [isLoadingModels, setIsLoadingModels] = useState(false) + const [isPulling, setIsPulling] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [pullModel, setPullModel] = useState('') + + // Endpoints + const [ollamaEndpoint, setOllamaEndpoint] = useState('http://localhost:11434') + const [bradieEndpoint, setBradieEndpoint] = useState('http://localhost:3001') + + // Bradie project management + const [projectName, setProjectName] = useState('') + const [projectPath, setProjectPath] = useState('') + const [isCreatingProject, setIsCreatingProject] = useState(false) + + // UI state + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [isTestingConnection, setIsTestingConnection] = useState(false) + const [selectedModel, setSelectedModel] = useState('') + const [sessionId, setSessionId] = useState(null) + const [projectId, setProjectId] = useState(null) + + const ollamaClient = new OllamaClient(ollamaEndpoint) + + // Test connections when dialog opens + useEffect(() => { + if (isOpen) { + testConnections() + loadOllamaModels() + } + }, [isOpen]) + + const testConnections = async () => { + setOllamaStatus('checking') + setBradieStatus('checking') + + try { + await ollamaClient.listModels() + setOllamaStatus('online') + } catch { + setOllamaStatus('offline') + } + + try { + const response = await fetch(`${bradieEndpoint}/api/health`) + setBradieStatus(response.ok ? 'online' : 'offline') + } catch { + setBradieStatus('offline') + } + } + + const loadOllamaModels = async () => { + if (ollamaStatus === 'offline') { + setOllamaModels([]) + return + } + + setIsLoadingModels(true) + try { + const models = await ollamaClient.listModels() + setOllamaModels(models || []) + } catch (err) { + console.error('Failed to load Ollama models:', err) + setOllamaModels([]) + } finally { + setIsLoadingModels(false) + } + } + + const handlePullModel = async () => { + if (!pullModel.trim()) return + + setIsPulling(true) + setError(null) + setSuccess(null) + + try { + await ollamaClient.pullModel(pullModel.trim()) + setSuccess(`Successfully pulled model: ${pullModel}`) + setPullModel('') + await loadOllamaModels() + } catch (err) { + setError(`Failed to pull model: ${err}`) + } finally { + setIsPulling(false) + } + } + + const handleDeleteModel = async (model: string) => { + if (!confirm(`Are you sure you want to delete the model "${model}"?`)) return + + setIsDeleting(true) + setError(null) + setSuccess(null) + + try { + await ollamaClient.deleteModel(model) + setSuccess(`Successfully deleted model: ${model}`) + await loadOllamaModels() + } catch (err) { + setError(`Failed to delete model: ${err}`) + } finally { + setIsDeleting(false) + } + } + + const handleTestConnection = async (provider: AgentProvider, endpoint: string) => { + setIsTestingConnection(true) + try { + let isOnline = false + if (provider === 'ollama') { + const testClient = new OllamaClient(endpoint) + await testClient.listModels() + isOnline = true + setOllamaStatus('online') + } else { + const response = await fetch(`${endpoint}/api/health`) + isOnline = response.ok + setBradieStatus(isOnline ? 'online' : 'offline') + } + setSuccess(`${provider} connection ${isOnline ? 'successful' : 'failed'}`) + } catch (err) { + if (provider === 'ollama') { + setOllamaStatus('offline') + } else { + setBradieStatus('offline') + } + setError(`Failed to test ${provider} connection: ${err}`) + } finally { + setIsTestingConnection(false) + } + } + + const handleCreateBradieProject = async () => { + if (!projectName.trim() || !projectPath.trim()) { + setError('Please fill in all fields') + return + } + + setIsCreatingProject(true) + setError(null) + + try { + // This would need Bradie client implementation + setSuccess(`Project creation not yet implemented`) + setProjectName('') + setProjectPath('') + } catch (err) { + setError(`Failed to create project: ${err}`) + } finally { + setIsCreatingProject(false) + } + } + + return ( + !open && onClose()}> + + + Agent Configuration + + Configure your AI agents using Agentic Kit - switch between Ollama and Bradie providers. + + + + + + Providers + Models + Projects + + + {/* Providers Tab */} + + {error && ( + + {error} + + )} + {success && ( + + + {success} + + + )} + + {/* Current Provider */} +
+ + +
+ + {/* Ollama Configuration */} +
+
+ +
+ {ollamaStatus === 'checking' ? ( + + ) : ollamaStatus === 'online' ? ( + + ) : ( + + )} + {ollamaStatus} +
+
+
+ setOllamaEndpoint(e.target.value)} + placeholder="http://localhost:11434" + /> + +
+
+ + {/* Bradie Configuration */} +
+
+ +
+ {bradieStatus === 'checking' ? ( + + ) : bradieStatus === 'online' ? ( + + ) : ( + + )} + {bradieStatus} +
+
+
+ setBradieEndpoint(e.target.value)} + placeholder="http://localhost:3001" + /> + +
+
+ + {/* Model Selection */} +
+ + setSelectedModel(e.target.value)} + placeholder="mistral" + /> +

+ Model name to use (e.g., mistral, llama2, codellama) +

+
+
+ + {/* Models Tab */} + +
+
+ + +
+ + {/* Available Models */} +
+ {isLoadingModels ? ( +
+ +
+ ) : !ollamaModels || ollamaModels.length === 0 ? ( +
+ {ollamaStatus === 'offline' ? ( + 'Cannot connect to Ollama. Please ensure it is running.' + ) : ( + 'No models installed. Pull a model to get started.' + )} +
+ ) : ( + (ollamaModels || []).map((model) => ( +
+
+
{model}
+ {model === selectedModel && ( +
Currently selected
+ )} +
+
+ {model !== selectedModel && ( + + )} + +
+
+ )) + )} +
+ + {/* Pull Model */} +
+ +
+ setPullModel(e.target.value)} + disabled={isPulling || ollamaStatus !== 'online'} + onKeyDown={(e) => { + if (e.key === 'Enter' && !isPulling) { + handlePullModel() + } + }} + /> + +
+

+ Popular models: llama2, mistral, codellama, mixtral, phi +

+
+
+
+ + {/* Projects Tab */} + +
+
+ +
+
+ Session ID: + {sessionId || 'None'} +
+
+ Project ID: + {projectId || 'None'} +
+
+ Status: + + {bradieStatus} + +
+
+
+ + {/* Create New Project */} +
+ + +
+ + setProjectName(e.target.value)} + /> +
+ +
+ +
+ setProjectPath(e.target.value)} + className="flex-1" + /> + +
+
+ + +
+
+
+
+ + + + +
+
+ ) +} \ No newline at end of file diff --git a/components/agent-manager-global.tsx b/components/agent-manager-global.tsx new file mode 100644 index 0000000..1223793 --- /dev/null +++ b/components/agent-manager-global.tsx @@ -0,0 +1,428 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Loader2, + CheckCircle, + AlertCircle, + Server, + Brain, + Sparkles, + Plus, + Trash2, + FolderOpen, + Globe +} from 'lucide-react' +import { OllamaClient, BradieClient, type Session } from '@/lib/agents' +import type { GlobalAgentConfig } from './ai-chat-global' + +interface AgentManagerGlobalProps { + isOpen: boolean + onClose: () => void + currentConfig: GlobalAgentConfig + onConfigChange: (config: GlobalAgentConfig) => void +} + +export function AgentManagerGlobal({ isOpen, onClose, currentConfig, onConfigChange }: AgentManagerGlobalProps) { + // Session Management State + const [sessions, setSessions] = useState([]) + const [showCreateSession, setShowCreateSession] = useState(false) + const [projectName, setProjectName] = useState('') + const [projectPath, setProjectPath] = useState('') + + // Model Management State + const [ollamaModels, setOllamaModels] = useState(['llama2', 'mistral', 'mixtral']) + const [isLoadingModels, setIsLoadingModels] = useState(false) + + // Endpoint State + const [bradieDomain, setBradieDomain] = useState(currentConfig.bradieDomain || 'http://localhost:3001') + const [ollamaEndpoint, setOllamaEndpoint] = useState(currentConfig.endpoint || 'http://localhost:11434') + const [isTestingConnection, setIsTestingConnection] = useState(false) + + // General State + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [connectionStatus, setConnectionStatus] = useState>({}) + + // Load sessions from localStorage + useEffect(() => { + const savedSessions = localStorage.getItem('ai-chat-bradie-sessions') + if (savedSessions) { + try { + const parsed = JSON.parse(savedSessions) + setSessions(parsed.map((s: any) => ({ + ...s, + createdAt: new Date(s.createdAt) + }))) + } catch (err) { + console.error('Failed to load sessions:', err) + } + } + }, []) + + // Save sessions to localStorage + const saveSessions = (newSessions: Session[]) => { + setSessions(newSessions) + localStorage.setItem('ai-chat-bradie-sessions', JSON.stringify(newSessions)) + } + + // Test Ollama connection and load models + const testOllamaConnection = async () => { + setIsTestingConnection(true) + setError(null) + + try { + const client = new OllamaClient(ollamaEndpoint) + const models = await client.listModels() + setOllamaModels(models) + setConnectionStatus(prev => ({ ...prev, ollama: true })) + setSuccess('Successfully connected to Ollama') + } catch (err) { + console.error('Ollama connection error:', err) + setConnectionStatus(prev => ({ ...prev, ollama: false })) + setError('Failed to connect to Ollama. Please ensure Ollama is running.') + } finally { + setIsTestingConnection(false) + } + } + + // Test Bradie connection + const testBradieConnection = async () => { + setIsTestingConnection(true) + setError(null) + + try { + const client = new BradieClient(bradieDomain) + const isHealthy = await client.checkHealth() + setConnectionStatus(prev => ({ ...prev, bradie: isHealthy })) + if (isHealthy) { + setSuccess('Successfully connected to Bradie') + } else { + setError('Bradie backend is not responding') + } + } catch (err) { + console.error('Bradie connection error:', err) + setConnectionStatus(prev => ({ ...prev, bradie: false })) + setError('Failed to connect to Bradie backend') + } finally { + setIsTestingConnection(false) + } + } + + // Create new Bradie session + const createSession = async () => { + if (!projectName || !projectPath) { + setError('Please provide both project name and path') + return + } + + try { + const client = new BradieClient(bradieDomain) + const session = await client.createSession(projectName, projectPath) + const newSessions = [...sessions, session] + saveSessions(newSessions) + + // Update config with new session + onConfigChange({ + ...currentConfig, + agent: 'bradie', + bradieDomain, + session + }) + + setShowCreateSession(false) + setProjectName('') + setProjectPath('') + setSuccess('Session created successfully') + } catch (err) { + console.error('Failed to create session:', err) + setError('Failed to create session') + } + } + + // Delete session + const deleteSession = (sessionId: string) => { + const newSessions = sessions.filter(s => s.id !== sessionId) + saveSessions(newSessions) + + // If deleted session was active, clear it + if (currentConfig.session?.id === sessionId) { + onConfigChange({ + ...currentConfig, + session: null + }) + } + } + + // Apply configuration + const applyConfig = () => { + const newConfig: GlobalAgentConfig = { + agent: currentConfig.agent, + endpoint: currentConfig.agent === 'ollama' ? ollamaEndpoint : currentConfig.endpoint, + model: currentConfig.model, + bradieDomain: bradieDomain, + session: currentConfig.session + } + + onConfigChange(newConfig) + onClose() + } + + return ( + + + + Agent Configuration + + Configure your AI agents and manage sessions + + + + onConfigChange({ ...currentConfig, agent: v as any })}> + + + + Ollama + + + + Bradie + + + + {/* Ollama Configuration */} + +
+ +
+ setOllamaEndpoint(e.target.value)} + placeholder="http://localhost:11434" + /> + +
+ {connectionStatus.ollama !== undefined && ( +
+ {connectionStatus.ollama ? ( + + ) : ( + + )} + {connectionStatus.ollama ? 'Connected' : 'Not connected'} +
+ )} +
+ + {ollamaModels.length > 0 && ( +
+ + +
+ )} +
+ + {/* Bradie Configuration */} + +
+ +
+ setBradieDomain(e.target.value)} + placeholder="http://localhost:3001" + /> + +
+ {connectionStatus.bradie !== undefined && ( +
+ {connectionStatus.bradie ? ( + + ) : ( + + )} + {connectionStatus.bradie ? 'Connected' : 'Not connected'} +
+ )} +
+ + {/* Sessions */} +
+
+ + +
+ + {sessions.length === 0 ? ( +

+ No sessions yet. Create one to get started. +

+ ) : ( +
+ {sessions.map((session) => ( +
onConfigChange({ ...currentConfig, session })} + > +
+ +
+

{session.name}

+

{session.path}

+
+
+ +
+ ))} +
+ )} +
+ + {/* Create Session Dialog */} + {showCreateSession && ( +
+

Create New Session

+
+ + setProjectName(e.target.value)} + placeholder="My Project" + /> +
+
+ + setProjectPath(e.target.value)} + placeholder="/path/to/project" + /> +
+
+ + +
+
+ )} +
+
+ + {/* Error/Success Messages */} + {error && ( + + + {error} + + )} + {success && ( + + + {success} + + )} + + + + + +
+
+ ) +} diff --git a/components/ai-chat-agentic.tsx b/components/ai-chat-agentic.tsx new file mode 100644 index 0000000..43a6ff7 --- /dev/null +++ b/components/ai-chat-agentic.tsx @@ -0,0 +1,518 @@ +'use client' + +import React, { useState, useRef, useEffect, KeyboardEvent } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { AgentManagerAgentic } from './agent-manager-agentic' +import { + AgentKit, + OllamaAdapter, + BradieAdapter, + createMultiProviderKit +} from 'agentic-kit' + +export type AgentProvider = 'ollama' | 'bradie' + +export interface ChatMessage { + id: string + role: 'user' | 'assistant' | 'system' + content: string + timestamp: Date + provider?: AgentProvider +} +import { + MoreVertical, + Send, + User, + Bot, + Copy, + Check, + ChevronRight, + MessageSquare, + Trash2, + Plus, + Pin, + PinOff, + Loader2, + AlertCircle, + Settings2, + Brain, + Sparkles +} from 'lucide-react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from '@/components/ui/dropdown-menu' + +interface AIChatAgenticProps { + isOpen: boolean + onToggle: () => void + width: number + onWidthChange: (width: number) => void + layoutMode: 'floating' | 'snapped' + onLayoutModeChange: (mode: 'floating' | 'snapped') => void +} + +export function AIChatAgentic({ + isOpen, + onToggle, + width, + onWidthChange, + layoutMode, + onLayoutModeChange +}: AIChatAgenticProps) { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [showTimestamps, setShowTimestamps] = useState(false) + const [copiedCode, setCopiedCode] = useState(null) + const [isResizing, setIsResizing] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [showAgentManager, setShowAgentManager] = useState(false) + const [streamingMessage, setStreamingMessage] = useState('') + + // Agent state + const [currentProvider, setCurrentProvider] = useState('ollama') + const [agentKit] = useState(() => { + const kit = createMultiProviderKit() + kit.addProvider(new OllamaAdapter('http://localhost:11434')) + kit.addProvider(new BradieAdapter({ + domain: 'http://localhost:3001', + onSystemMessage: () => {}, + onAssistantReply: () => {} + })) + kit.setProvider('ollama') + return kit + }) + + const resizeRef = useRef(null) + const chatEndRef = useRef(null) + + // Auto scroll to bottom on new messages + useEffect(() => { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages, streamingMessage]) + + // Load chat history from localStorage (use same key as global chat) + useEffect(() => { + const savedMessages = localStorage.getItem('ai-chat-messages') + if (savedMessages) { + try { + const parsed = JSON.parse(savedMessages) + setMessages(parsed.map((m: any) => ({ + ...m, + timestamp: new Date(m.timestamp) + }))) + } catch (err) { + console.error('Failed to load chat history:', err) + } + } + }, []) + + // Save messages to localStorage (use same key as global chat) + useEffect(() => { + if (messages.length > 0) { + localStorage.setItem('ai-chat-messages', JSON.stringify(messages)) + } + }, [messages]) + + // Handle provider change + const handleProviderChange = (provider: AgentProvider) => { + setCurrentProvider(provider) + agentKit.setProvider(provider) + } + + // Handle send message + const handleSend = async () => { + if (!input.trim() || isLoading) return + + const userMessage: ChatMessage = { + id: Date.now().toString(), + role: 'user', + content: input.trim(), + timestamp: new Date(), + provider: currentProvider + } + + setMessages(prev => [...prev, userMessage]) + setInput('') + setIsLoading(true) + setError(null) + setStreamingMessage('') + + try { + let fullResponse = '' + + // Handle streaming + const onChunk = (chunk: string) => { + fullResponse += chunk + setStreamingMessage(fullResponse) + } + + // Generate response + await agentKit.generate( + { + model: 'mistral', + prompt: userMessage.content.toLowerCase().includes('hyperweb') + ? `# Hyperweb Smart Contract Guide + +## Overview +Hyperweb smart contracts are written in TypeScript and follow a specific class-based structure. Each contract must have a state interface and a main contract class. + +## Basic Structure +1. Define a state interface that describes your contract's data +2. Create a contract class that extends the state interface +3. Implement methods to interact with the state + +## Example Contract +\`\`\`typescript +// Define the state interface +interface HelloWorldState { + greeting: string; +} + +// Create the contract class +export default class HelloWorldContract { + // Initialize state with default values + state: HelloWorldState = { greeting: "Hello, world!" }; + + // Constructor is called when contract is deployed + constructor() { + console.log("[HelloWorldContract] Constructor called."); + } + + // Methods can be public or private + // Public methods can be called by users + // Private methods (prefixed with _) are internal only + + // Initialize contract with parameters + init({ greeting }: { greeting: string }): void { + this.state.greeting = greeting; + } + + // Getter method to read state + getGreeting(): string { + return this.state.greeting; + } + + // Setter method to modify state + setGreeting({ greeting }: { greeting: string }): string { + this.state.greeting = greeting; + return this.state.greeting; + } +} +\`\`\` + +## Key Concepts +1. State Interface: Defines the shape of your contract's data +2. Contract Class: Implements the business logic +3. Methods: Functions that interact with the state +4. Types: Use TypeScript types for all parameters and return values +5. Console Logging: Use console.log for debugging + +## Best Practices +1. Always define a state interface +2. Use TypeScript types for all parameters +3. Return values from methods when appropriate +4. Use descriptive method names +5. Include console.log statements for debugging + +## User Query +\`${userMessage.content}\`` + : userMessage.content, + stream: true + }, + { + onChunk + } + ) + + // After streaming completes, create the final message + if (fullResponse.trim()) { + const assistantMessage: ChatMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: fullResponse, + timestamp: new Date(), + provider: currentProvider + } + setMessages(prev => [...prev, assistantMessage]) + } + + // Clear streaming message + setStreamingMessage('') + + } catch (err) { + console.error('Error generating response:', err) + setError('An error occurred. Please try again.') + setStreamingMessage('') + } finally { + setIsLoading(false) + } + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + if (!isLoading) { + handleSend() + } + } + } + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text) + setCopiedCode(text) + setTimeout(() => setCopiedCode(null), 2000) + } catch (err) { + console.error('Failed to copy:', err) + } + } + + const clearHistory = () => { + setMessages([]) + localStorage.removeItem('ai-chat-messages') + } + + // Handle resize + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing) return + const newWidth = window.innerWidth - e.clientX + onWidthChange(Math.max(300, Math.min(800, newWidth))) + } + + const handleMouseUp = () => { + setIsResizing(false) + } + + if (isResizing) { + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + }, [isResizing, onWidthChange]) + + if (!isOpen) return null + + const containerStyle = layoutMode === 'floating' + ? 'fixed right-4 top-4 bottom-4 shadow-2xl rounded-lg border' + : 'fixed right-0 top-0 bottom-0 border-l' + + return ( + <> +
+ {/* Resize handle */} +
setIsResizing(true)} + /> + + {/* Header */} +
+
+
+ {currentProvider === 'ollama' ? ( + + ) : ( + + )} + {currentProvider} +
+
+
+ + + + + + + handleProviderChange(currentProvider === 'ollama' ? 'bradie' : 'ollama')}> + Switch to {currentProvider === 'ollama' ? 'Bradie' : 'Ollama'} + + setShowTimestamps(!showTimestamps)}> + {showTimestamps ? 'Hide' : 'Show'} Timestamps + + onLayoutModeChange(layoutMode === 'floating' ? 'snapped' : 'floating')}> + {layoutMode === 'floating' ? : } + {layoutMode === 'floating' ? 'Snap to Side' : 'Float'} + + + + + Clear History + + + + +
+
+ + {/* Messages */} +
+ {messages.length === 0 && !streamingMessage && ( +
+ +

Start a conversation with {currentProvider === 'ollama' ? 'Ollama' : 'Bradie'}

+
+ )} + + {messages.map((message) => ( +
+ {message.role === 'assistant' && ( +
+ +
+ )} +
+
+ {message.role === 'assistant' ? ( + + {React.createElement(SyntaxHighlighter as any, { + style: vscDarkPlus, + language: match[1], + PreTag: "div", + ...props + }, String(children).replace(/\n$/, ''))} + +
+ ) : ( + + {children} + + ) + } + }} + > + {message.content} + + ) : ( +

{message.content}

+ )} +
+ {showTimestamps && ( +

+ {message.timestamp.toLocaleTimeString()} +

+ )} +
+ {message.role === 'user' && ( +
+ +
+ )} +
+ ))} + + {/* Streaming message */} + {streamingMessage && ( +
+
+ +
+
+ + {streamingMessage} + +
+
+ )} + + {isLoading && !streamingMessage && ( +
+
+ +
+
+ +
+
+ )} + + {error && ( +
+ +

{error}

+
+ )} + +
+
+ + {/* Input */} +
+
+