UI Integration

Tarko provides flexible UI integration options to build user interfaces for your agents using modern web technologies.

Integration Options

The official web UI implementation that works out-of-the-box with any Tarko Agent:

npm install @tarko/agent-ui

Features:

  • Real-time agent communication
  • Built-in chat interface
  • Tool execution visualization
  • Event stream monitoring
  • Responsive design

2. Custom Web UI

Build your own web interface using the Agent Protocol:

import { AgentClient } from '@tarko/agent-client';

const client = new AgentClient({
  endpoint: 'http://localhost:3000',
});

// Send message to agent
const response = await client.sendMessage('Hello, agent!');

3. Native Applications

Integrate with desktop or mobile applications using HTTP/WebSocket APIs.

Architecture Overview

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Frontend UI   │◄──►│  Tarko Agent     │◄──►│  LLM Provider   │
│                 │    │  Server          │    │                 │
├─────────────────┤    ├──────────────────┤    ├─────────────────┤
│ • Chat Interface│    │ • Agent Protocol │    │ • OpenAI        │
│ • Tool Outputs  │    │ • Event Stream   │    │ • Anthropic     │
│ • Real-time     │    │ • Tool Execution │    │ • Volcengine    │
│   Updates       │    │ • Context Mgmt   │    │ • Others        │
└─────────────────┘    └──────────────────┘    └─────────────────┘

Quick Start with Tarko Agent UI

The fastest way to get a web UI for your agent:

1. Install Dependencies

npm install @tarko/agent-ui

2. Basic Setup

import { AgentUI } from '@tarko/agent-ui';
import '@tarko/agent-ui/styles.css';

function App() {
  return (
    <AgentUI
      endpoint="http://localhost:3000"
      title="My Agent"
      theme="light"
    />
  );
}

export default App;

3. Start Your Agent Server

tarko run --server

Your web UI will connect to the agent automatically!

Communication Protocols

HTTP API

RESTful API for basic agent interactions:

// Send message
POST /api/chat
{
  "message": "Hello, agent!",
  "sessionId": "session-123"
}

// Get session history
GET /api/sessions/session-123/messages

WebSocket

Real-time bidirectional communication:

const ws = new WebSocket('ws://localhost:3000/ws');

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Agent event:', data);
};

Server-Sent Events (SSE)

Streaming responses for real-time updates:

const eventSource = new EventSource('/api/stream/session-123');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Stream update:', data);
};

Event Stream

Tarko uses a standardized event stream format for real-time communication:

interface AgentEvent {
  type: 'message' | 'tool_call' | 'tool_result' | 'thinking' | 'error';
  timestamp: string;
  sessionId: string;
  data: any;
}

Event Types

Event TypeDescriptionData
messageAgent response message{ content: string, role: 'assistant' }
tool_callTool execution started{ name: string, args: object }
tool_resultTool execution completed{ result: any, success: boolean }
thinkingAgent reasoning process{ content: string }
errorError occurred{ message: string, code?: string }

Custom Web UI Development

React Integration

Build a custom React interface:

import React, { useState, useEffect } from 'react';
import { AgentClient } from '@tarko/agent-client';

const CustomAgentUI = () => {
  const [client] = useState(() => new AgentClient({
    endpoint: 'http://localhost:3000'
  }));
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);

  const sendMessage = async () => {
    if (!input.trim()) return;
    
    setLoading(true);
    const userMessage = { role: 'user', content: input };
    setMessages(prev => [...prev, userMessage]);
    setInput('');

    try {
      const response = await client.sendMessage(input);
      const assistantMessage = { role: 'assistant', content: response.content };
      setMessages(prev => [...prev, assistantMessage]);
    } catch (error) {
      console.error('Error sending message:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="agent-ui">
      <div className="messages">
        {messages.map((msg, idx) => (
          <div key={idx} className={`message ${msg.role}`}>
            {msg.content}
          </div>
        ))}
      </div>
      <div className="input-area">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type your message..."
          disabled={loading}
        />
        <button onClick={sendMessage} disabled={loading}>
          {loading ? 'Sending...' : 'Send'}
        </button>
      </div>
    </div>
  );
};

Vue.js Integration

<template>
  <div class="agent-ui">
    <div class="messages">
      <div
        v-for="(message, index) in messages"
        :key="index"
        :class="`message ${message.role}`"
      >
        {{ message.content }}
      </div>
    </div>
    <div class="input-area">
      <input
        v-model="input"
        @keyup.enter="sendMessage"
        placeholder="Type your message..."
        :disabled="loading"
      />
      <button @click="sendMessage" :disabled="loading">
        {{ loading ? 'Sending...' : 'Send' }}
      </button>
    </div>
  </div>
</template>

<script>
import { AgentClient } from '@tarko/agent-client';

export default {
  data() {
    return {
      client: new AgentClient({ endpoint: 'http://localhost:3000' }),
      messages: [],
      input: '',
      loading: false
    };
  },
  methods: {
    async sendMessage() {
      if (!this.input.trim()) return;
      
      this.loading = true;
      this.messages.push({ role: 'user', content: this.input });
      const message = this.input;
      this.input = '';

      try {
        const response = await this.client.sendMessage(message);
        this.messages.push({ role: 'assistant', content: response.content });
      } catch (error) {
        console.error('Error:', error);
      } finally {
        this.loading = false;
      }
    }
  }
};
</script>

Real-time Features

WebSocket Connection

Implement real-time communication:

class AgentWebSocket {
  private ws: WebSocket;
  private eventHandlers: Map<string, Function[]> = new Map();

  constructor(endpoint: string) {
    this.ws = new WebSocket(endpoint.replace('http', 'ws') + '/ws');
    this.setupEventHandlers();
  }

  private setupEventHandlers() {
    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      const handlers = this.eventHandlers.get(data.type) || [];
      handlers.forEach(handler => handler(data));
    };

    this.ws.onopen = () => {
      console.log('WebSocket connected');
    };

    this.ws.onclose = () => {
      console.log('WebSocket disconnected');
      // Implement reconnection logic
    };
  }

  on(eventType: string, handler: Function) {
    if (!this.eventHandlers.has(eventType)) {
      this.eventHandlers.set(eventType, []);
    }
    this.eventHandlers.get(eventType)!.push(handler);
  }

  sendMessage(message: string) {
    this.ws.send(JSON.stringify({ type: 'message', content: message }));
  }
}

// Usage
const agentWS = new AgentWebSocket('http://localhost:3000');

agentWS.on('message', (data) => {
  console.log('Received message:', data.content);
});

agentWS.on('tool_call', (data) => {
  console.log('Tool called:', data.name, data.args);
});

agentWS.on('tool_result', (data) => {
  console.log('Tool result:', data.result);
});

Server-Sent Events

Alternative approach using SSE:

class AgentEventSource {
  private eventSource: EventSource;
  private sessionId: string;

  constructor(endpoint: string, sessionId: string) {
    this.sessionId = sessionId;
    this.eventSource = new EventSource(`${endpoint}/api/stream/${sessionId}`);
    this.setupEventHandlers();
  }

  private setupEventHandlers() {
    this.eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.handleEvent(data);
    };

    this.eventSource.onerror = (error) => {
      console.error('SSE error:', error);
    };
  }

  private handleEvent(data: any) {
    switch (data.type) {
      case 'message':
        this.onMessage(data);
        break;
      case 'tool_call':
        this.onToolCall(data);
        break;
      case 'tool_result':
        this.onToolResult(data);
        break;
    }
  }

  onMessage(data: any) {
    // Override in subclass or pass callback
  }

  onToolCall(data: any) {
    // Override in subclass or pass callback
  }

  onToolResult(data: any) {
    // Override in subclass or pass callback
  }

  close() {
    this.eventSource.close();
  }
}

UI Components

Chat Interface

Basic chat component structure:

interface ChatMessage {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: Date;
  toolCalls?: ToolCall[];
}

interface ToolCall {
  id: string;
  name: string;
  args: object;
  result?: any;
  status: 'pending' | 'success' | 'error';
}

Tool Execution Visualization

import React from 'react';

interface ToolExecutionProps {
  toolCall: {
    name: string;
    args: object;
    result?: any;
    status: 'pending' | 'success' | 'error';
    startTime: Date;
    endTime?: Date;
  };
}

const ToolExecution: React.FC<ToolExecutionProps> = ({ toolCall }) => {
  const duration = toolCall.endTime 
    ? toolCall.endTime.getTime() - toolCall.startTime.getTime()
    : null;

  return (
    <div className={`tool-execution ${toolCall.status}`}>
      <div className="tool-header">
        <span className="tool-name">{toolCall.name}</span>
        <span className="tool-status">{toolCall.status}</span>
        {duration && (
          <span className="tool-duration">{duration}ms</span>
        )}
      </div>
      
      <details className="tool-args">
        <summary>Arguments</summary>
        <pre>{JSON.stringify(toolCall.args, null, 2)}</pre>
      </details>
      
      {toolCall.result && (
        <details className="tool-result">
          <summary>Result</summary>
          <pre>{JSON.stringify(toolCall.result, null, 2)}</pre>
        </details>
      )}
    </div>
  );
};

Thinking Process Display

const ThinkingProcess: React.FC<{ thoughts: string[] }> = ({ thoughts }) => {
  return (
    <div className="thinking-process">
      <div className="thinking-header">
        <span>🤔 Agent is thinking...</span>
      </div>
      <div className="thoughts">
        {thoughts.map((thought, idx) => (
          <div key={idx} className="thought">
            {thought}
          </div>
        ))}
      </div>
    </div>
  );
};

Styling and Theming

CSS Variables

:root {
  --agent-primary: #007bff;
  --agent-secondary: #6c757d;
  --agent-success: #28a745;
  --agent-danger: #dc3545;
  --agent-warning: #ffc107;
  --agent-info: #17a2b8;
  
  --agent-bg: #ffffff;
  --agent-text: #333333;
  --agent-border: #e9ecef;
  
  --agent-message-user-bg: #007bff;
  --agent-message-user-text: #ffffff;
  --agent-message-assistant-bg: #f8f9fa;
  --agent-message-assistant-text: #333333;
}

[data-theme="dark"] {
  --agent-bg: #1a1a1a;
  --agent-text: #ffffff;
  --agent-border: #333333;
  
  --agent-message-assistant-bg: #2d2d2d;
  --agent-message-assistant-text: #ffffff;
}

Component Styles

.agent-ui {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: var(--agent-bg);
  color: var(--agent-text);
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
}

.message {
  margin-bottom: 1rem;
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  max-width: 80%;
}

.message.user {
  background: var(--agent-message-user-bg);
  color: var(--agent-message-user-text);
  margin-left: auto;
}

.message.assistant {
  background: var(--agent-message-assistant-bg);
  color: var(--agent-message-assistant-text);
}

.input-area {
  display: flex;
  padding: 1rem;
  border-top: 1px solid var(--agent-border);
}

.input-area input {
  flex: 1;
  padding: 0.75rem;
  border: 1px solid var(--agent-border);
  border-radius: 0.25rem;
  margin-right: 0.5rem;
}

.input-area button {
  padding: 0.75rem 1.5rem;
  background: var(--agent-primary);
  color: white;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
}

.input-area button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Authentication & Security

API Keys

Secure API key authentication:

const client = new AgentClient({
  endpoint: 'http://localhost:3000',
  apiKey: process.env.TARKO_API_KEY,
});

Session Management

Manage user sessions and context:

interface Session {
  id: string;
  userId?: string;
  createdAt: Date;
  lastActivity: Date;
  context: AgentContext;
}

Deployment Considerations

CORS Configuration

For web UIs, configure CORS in your agent server:

export default defineConfig({
  server: {
    cors: {
      origin: ['http://localhost:3000', 'https://myapp.com'],
      credentials: true,
    },
  },
});

Reverse Proxy

Use a reverse proxy for production deployments:

location /api/ {
    proxy_pass http://localhost:3001/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
}

Environment Configuration

// config.ts
export const config = {
  apiEndpoint: process.env.REACT_APP_API_ENDPOINT || 'http://localhost:3000',
  wsEndpoint: process.env.REACT_APP_WS_ENDPOINT || 'ws://localhost:3000',
  apiKey: process.env.REACT_APP_API_KEY,
};

Build and Deploy

# Build for production
npm run build

# Deploy to static hosting
# (Vercel, Netlify, AWS S3, etc.)

Docker Deployment

# Dockerfile
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=0 /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Next Steps