From a1b7191098521226919f3dbfee384c3e9a4c9504 Mon Sep 17 00:00:00 2001 From: truetype Date: Tue, 10 Feb 2026 10:39:18 +0100 Subject: [PATCH] Initial commit: MUDlet AI Client with Telnet + Ollama integration --- README.md | 108 ++++++ docs/API.md | 373 ++++++++++++++++++++ docs/ARCHITECTURE.md | 273 ++++++++++++++ docs/WORKFLOW.md | 287 +++++++++++++++ package.json | 31 ++ postcss.config.js | 6 + src/main/index.ts | 112 ++++++ src/main/services/AutoplayerService.ts | 224 ++++++++++++ src/main/services/OllamaService.ts | 187 ++++++++++ src/main/services/TelnetService.ts | 129 +++++++ src/preload/index.js | 21 ++ src/renderer/App.tsx | 46 +++ src/renderer/components/AutoplayerPanel.tsx | 148 ++++++++ src/renderer/components/ConnectionPanel.tsx | 72 ++++ src/renderer/components/GameWindow.tsx | 60 ++++ src/renderer/index.css | 32 ++ src/renderer/index.html | 12 + src/renderer/main.tsx | 10 + tailwind.config.js | 11 + tsconfig.json | 25 ++ tsconfig.node.json | 10 + vite.config.ts | 20 ++ 22 files changed, 2197 insertions(+) create mode 100644 README.md create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/WORKFLOW.md create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 src/main/index.ts create mode 100644 src/main/services/AutoplayerService.ts create mode 100644 src/main/services/OllamaService.ts create mode 100644 src/main/services/TelnetService.ts create mode 100644 src/preload/index.js create mode 100644 src/renderer/App.tsx create mode 100644 src/renderer/components/AutoplayerPanel.tsx create mode 100644 src/renderer/components/ConnectionPanel.tsx create mode 100644 src/renderer/components/GameWindow.tsx create mode 100644 src/renderer/index.css create mode 100644 src/renderer/index.html create mode 100644 src/renderer/main.tsx create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..997a9ad --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# MUDlet AI Client + +Un client MUD moderno con integrazione LLM per automazione del gameplay, sviluppato con Electron + React + TypeScript. + +## Features + +### Core +- **Connessione Telnet** - Supporto completo per protocollo Telnet con parsing ANSI colors +- **Game Window** - Display in tempo reale con input commands +- **Connection Panel** - Gestione connessioni con host/porta configurabili + +### Autoplayer LLM +- **Ollama Integration** - Connessione a server Ollama remoto (ollama.solarpi.net) +- **Decision Engine** - Sistema AI che analizza l'output del gioco e genera comandi +- **System Prompt** - Personalizzazione del comportamento del bot +- **Model Selection** - Supporto per modelli multipli (llama3.2, mistral, etc.) +- **Action Queue** - Coda azioni per revisione manuale o auto-send + +### UI/UX +- **React + TypeScript** - Codice type-safe con React 18 +- **Tailwind CSS** - Styling moderno e responsive +- **Navigation** - Pannelli Connection/Game/Autoplayer + +## Quick Start + +```bash +# Clona e installa dipendenze +cd mudlet-ai-client +npm install + +# Avvia dev server (necessario per Electron) +npm run dev + +# Avvia Electron (in terminale separato) +npm start +``` + +## Connessione + +Server LPMUD preconfigurato: +- **Host**: 172.24.19.225 +- **Porta**: 6666 + +Server Ollama: +- **URL**: http://ollama.solarpi.net + +## Stack Tecnologico + +| Layer | Tecnologie | +|-------|------------| +| Framework | Electron 28, React 18, TypeScript 5 | +| UI | Tailwind CSS, shadcn/ui | +| Telnet | telnet-client, ansicolor | +| LLM | Ollama (HTTP API) | +| Build | Vite 5, electron-vite | + +## Struttura Progetto + +``` +mudlet-ai-client/ +├── src/ +│ ├── main/ # Electron main process +│ │ ├── index.ts # App entry point, IPC handlers +│ │ └── services/ +│ │ ├── TelnetService.ts # Connessione Telnet +│ │ ├── OllamaService.ts # Client HTTP Ollama +│ │ └── AutoplayerService.ts # Decision engine +│ ├── renderer/ # React UI +│ │ ├── App.tsx # Root component +│ │ ├── main.tsx # Entry point +│ │ └── components/ +│ │ ├── GameWindow.tsx # Game display + input +│ │ ├── ConnectionPanel.tsx +│ │ └── AutoplayerPanel.tsx +│ └── preload/ +│ └── index.js # Context bridge +├── package.json +├── vite.config.ts +└── tsconfig.json +``` + +## Workflow Autoplayer + +``` +1. OBSERVE → TelnetService riceve output +2. PARSE → AutoplayerService analizza stato +3. THINK → Ollama genera decisione +4. ACT → Azione eseguita o messa in coda +``` + +## Roadmap + +- [x] Fase 1: Client MUD Base +- [x] Fase 2: Integrazione Ollama +- [ ] Fase 3: Autoplayer miglioramenti (mapper 2D, trigger system) +- [ ] Fase 4: Sistema Plugin +- [ ] Fase 5: Funzionalità avanzate (persistence SQLite) + +## Documentazione + +- [Architettura](docs/ARCHITECTURE.md) +- [Workflow di Sviluppo](docs/WORKFLOW.md) +- [API IPC](docs/API.md) +- [Piano Progetto](MUD_AUTOPLAYER_PLAN.md) + +## License + +MIT diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..f8e0d44 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,373 @@ +# API IPC Reference + +## Context Bridge + +Tutte le API sono esposte tramite `window.electronAPI`: + +```typescript +// TypeScript declaration +interface ElectronAPI { + // Telnet + telnetConnect: (host: string, port: number) => Promise; + telnetDisconnect: () => Promise; + telnetSend: (data: string) => Promise; + telnetGetOutput: () => Promise; + onTelnetOutput: (callback: (data: string) => void) => void; + + // Ollama + ollamaChat: (messages: OllamaMessage[], options?: OllamaOptions) => Promise; + ollamaGetModels: () => Promise; + ollamaIsAvailable: () => Promise; + + // Autoplayer + autoplayerConfigure: (config: AutoplayerConfig) => Promise; + autoplayerStart: () => Promise; + autoplayerStop: () => Promise; + autoplayerIsActive: () => Promise; + autoplayerExecuteAction: (action: string) => Promise; + onAutoplayerDecision: (callback: (decision: string) => void) => void; + onAutoplayerAction: (callback: (action: string) => void) => void; + onAutoplayerState: (callback: (state: GameState) => void) => void; +} +``` + +--- + +## Telnet API + +### telnetConnect + +Stabilisce connessione Telnet al server MUD. + +```typescript +const connected = await window.electronAPI.telnetConnect('172.24.19.225', 6666); +``` + +**Params:** +| Param | Type | Description | +|-------|------|-------------| +| host | string | Indirizzo server MUD | +| port | number | Porta Telnet | + +**Returns:** `Promise` - true se connessione riuscita + +**Errors:** Connection timeout, host unreachable + +### telnetDisconnect + +Chiude connessione Telnet. + +```typescript +await window.electronAPI.telnetDisconnect(); +``` + +### telnetSend + +Invia comando al server MUD. + +```typescript +const sent = await window.electronAPI.telnetSend('look\n'); +``` + +**Params:** +| Param | Type | Description | +|-------|------|-------------| +| data | string | Comando da inviare (newline esclusa) | + +**Returns:** `Promise` - true se invio riuscito + +### telnetGetOutput + +Restituisce buffer output Telnet accumulato. + +```typescript +const output = await window.electronAPI.telnetGetOutput(); +``` + +**Returns:** `Promise` - Output completo con HTML colors + +### onTelnetOutput + +Subscribe a nuovi output dal server. + +```typescript +window.electronAPI.onTelnetOutput((data) => { + console.log('Nuovo output:', data); +}); +``` + +**Callback Params:** +| Param | Type | Description | +|-------|------|-------------| +| data | string | Chunk output HTML | + +--- + +## Ollama API + +### ollamaChat + +Invia messaggi a Ollama e riceve risposta. + +```typescript +const response = await window.electronAPI.ollamaChat([ + { role: 'system', content: 'You are a MUD player' }, + { role: 'user', content: 'What should I do?' } +], { + model: 'llama3.2', + temperature: 0.7 +}); +``` + +**Params:** +| Param | Type | Description | +|-------|------|-------------| +| messages | OllamaMessage[] | Storico conversazione | +| options | OllamaOptions | Opzioni (opzionali) | + +**OllamaMessage:** +```typescript +{ + role: 'user' | 'assistant' | 'system'; + content: string; +} +``` + +**OllamaOptions:** +```typescript +{ + model?: string; // Default: 'llama3.2' + temperature?: number; // Default: 0.7 +} +``` + +**Returns:** `Promise` +```typescript +{ + model: string; + response: string; + done: boolean; +} +``` + +### ollamaGetModels + +Lista modelli disponibili sul server. + +```typescript +const models = await window.electronAPI.ollamaGetModels(); +// ['llama3.2', 'llama3.1', 'mistral', ...] +``` + +**Returns:** `Promise` - Nomi modelli + +### ollamaIsAvailable + +Verifica se server Ollama raggiungibile. + +```typescript +const available = await window.electronAPI.ollamaIsAvailable(); +// true se server risponde +``` + +**Returns:** `Promise` + +--- + +## Autoplayer API + +### autoplayerConfigure + +Configura comportamento autoplayer. + +```typescript +await window.electronAPI.autoplayerConfigure({ + model: 'llama3.2', + systemPrompt: 'You are an explorer...', + temperature: 0.7, + autoSend: true +}); +``` + +**AutoplayerConfig:** +```typescript +{ + model: string; // Model LLM + systemPrompt: string; // Personalità bot + temperature?: number; // Creatività (0.0-1.0) + autoSend?: boolean; // Auto-esegui azioni +} +``` + +### autoplayerStart + +Avvia loop autoplayer. + +```typescript +await window.electronAPI.autoplayerStart(); +``` + +### autoplayerStop + +Ferma loop autoplayer. + +```typescript +await window.electronAPI.autoplayerStop(); +``` + +### autoplayerIsActive + +Verifica se autoplayer in esecuzione. + +```typescript +const active = await window.electronAPI.autoplayerIsActive(); +``` + +**Returns:** `Promise` + +### autoplayerExecuteAction + +Esegui azione manualmente (senza passare per decision engine). + +```typescript +await window.electronAPI.autoplayerExecuteAction('go north'); +``` + +### onAutoplayerDecision + +Subscribe a decisioni LLM. + +```typescript +window.electronAPI.onAutoplayerDecision((decision) => { + console.log('LLM decision:', decision); +}); +``` + +**Callback Params:** +| Param | Type | Description | +|-------|------|-------------| +| decision | string | Risposta completa LLM | + +### onAutoplayerAction + +Subscribe ad azioni generate. + +```typescript +window.electronAPI.onAutoplayerAction((action) => { + console.log('Action to execute:', action); +}); +``` + +**Callback Params:** +| Param | Type | Description | +|-------|------|-------------| +| action | string | Comando da eseguire | + +### onAutoplayerState + +Subscribe a updates stato gioco parsato. + +```typescript +window.electronAPI.onAutoplayerState((state) => { + console.log('Current location:', state.location); + console.log('Exits:', state.exits); +}); +``` + +**GameState:** +```typescript +{ + location: string; // Nome location corrente + description: string; // Descrizione stanza + exits: string[]; // Uscite disponibili + players: string[]; // Giocatori presenti + objects: string[]; // Oggetti nella stanza + health?: string; // HP corrente + mana?: string; // Mana corrente + prompt?: string; // Prompt del gioco +} +``` + +--- + +## Esempi Completi + +### Connessione e Game Loop + +```typescript +// 1. Connetti +const connected = await window.electronAPI.telnetConnect('172.24.19.225', 6666); +if (!connected) { + console.error('Connessione fallita'); + return; +} + +// 2. Subscribe output +window.electronAPI.onTelnetOutput((data) => { + // Aggiorna UI + setOutput(prev => prev + data); +}); + +// 3. Invia comando +await window.electronAPI.telnetSend('look\n'); +``` + +### Autoplayer Workflow + +```typescript +// 1. Configura +await window.electronAPI.autoplayerConfigure({ + model: 'llama3.2', + systemPrompt: 'You are a helpful MUD player. Explore and interact naturally.', + temperature: 0.7, + autoSend: false // Review manuale azioni +}); + +// 2. Subscribe decisioni +window.electronAPI.onAutoplayerAction((action) => { + console.log('Pending action:', action); + // Possibile review manuale qui +}); + +// 3. Avvia +await window.electronAPI.autoplayerStart(); + +// 4. Per fermare +await window.electronAPI.autoplayerStop(); +``` + +### Ollama Chat Diretta + +```typescript +// Test chat con Ollama +const response = await window.electronAPI.ollamaChat([ + { role: 'system', content: 'You are a fantasy game master.' }, + { role: 'user', content: 'Describe a dark forest.' } +], { temperature: 0.8 }); + +console.log(response.response); +``` + +--- + +## Error Handling + +```typescript +try { + await window.electronAPI.telnetConnect(host, port); +} catch (error) { + console.error('Connection error:', error); +} + +// Event-based errors +window.electronAPI.onTelnetOutput((data) => { + // Gestisci errori in output +}); +``` + +## Note + +- Tutte le chiamate IPC sono asincrone (Promise-based) +- Gli eventi devono essere sottoscritti una volta (non in useEffect ripetuti) +- Cleanup event listeners quando il componente unmount +- Utilizzare `window.electronAPI` solo dopo il mount del componente diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b153863 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,273 @@ +# Architettura + +## Panoramica + +MUDlet AI Client usa un'architettura a 3 layer basata su Electron: + +``` +┌─────────────────────────────────────────────────────────┐ +│ ELECTRON MAIN │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ +│ │ TelnetService│ │ OllamaService│ │ AutoplayerService│ │ +│ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │ +│ │ │ │ │ +│ └────────────────┼───────────────────┘ │ +│ ▼ │ +│ ┌─────────────────────┐ │ +│ │ IPC Handlers │ │ +│ └─────────┬───────────┘ │ +└────────────────────────┼────────────────────────────────┘ + │ +┌────────────────────────┼────────────────────────────────┐ +│ PRELOAD JS │ +│ contextBridge.exposeInMainWorld() │ +└────────────────────────┼────────────────────────────────┘ + │ +┌────────────────────────┼────────────────────────────────┐ +│ REACT RENDERER │ +│ ┌─────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ App │ │ GameWindow │ │ AutoplayerPanel │ │ +│ └─────────┘ └─────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Layer 1: Electron Main Process + +### Main Entry (`src/main/index.ts`) + +Responsabilità: +- Inizializzazione finestra BrowserWindow +- Setup IPC handlers +- Event forwarding tra servizi e renderer +- Lifecycle management (app ready, window closed, etc.) + +```typescript +// Flusso dati Telnet +telnetService.on('output', (data) => { + mainWindow.webContents.send('telnet-output', data); + autoplayerService.processOutput(data); // Forward all'autoplayer +}); +``` + +### TelnetService (`src/main/services/TelnetService.ts`) + +Wrapper attorno a `telnet-client` con funzionalità aggiuntive: + +| Metodo | Descrizione | +|--------|-------------| +| `connect(host, port)` | Stabilisce connessione Telnet | +| `send(data)` | Invia comando al server | +| `getOutput()` | Restituisce buffer output | +| `parseAnsiCodes(text)` | Converte ANSI escape codes in HTML | + +**Eventi:** +- `output` - Emesso quando arrives nuovo output +- `connected` - Connessione stabilita +- `disconnected` - Connessione chiusa +- `error` - Errore di connessione + +**Parsing ANSI:** +```typescript +// ansicolor converte sequenze ANSI in spans colorati +// Ogni span viene convertito in +``` + +### OllamaService (`src/main/services/OllamaService.ts`) + +Client HTTP per Ollama API. Supporta: + +| Metodo | Descrizione | +|--------|-------------| +| `chat(messages, options)` | Chat singola, attesa risposta | +| `chatStream(messages, options)` | Streaming response token-by-token | +| `getLocalModels()` | Lista modelli disponibili | +| `isAvailable()` | Check server reachability | + +**API Endpoint:** +``` +POST http://ollama.solarpi.net/api/chat +{ + "model": "llama3.2", + "messages": [...], + "stream": true, + "options": { + "temperature": 0.7, + "num_predict": 512 + } +} +``` + +### AutoplayerService (`src/main/services/AutoplayerService.ts`) + +Decision engine che orchestrate il loop autoplayer: + +``` +┌─────────────────────────────────────────────────────────┐ +│ AUTOPLAYER LOOP │ +│ │ +│ 1. OBSERVE ← processOutput(output) │ +│ 2. PARSE ← parseGameState() │ +│ 3. THINK ← ollamaService.chat() │ +│ 4. DECIDE ← extractAction() │ +│ 5. ACT ← executeAction() / queueAction() │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +**Configurazione:** +```typescript +interface AutoplayerConfig { + model: string; // Model LLM da usare + systemPrompt: string; // Comportamento del bot + temperature: number; // Creatività (0.0-1.0) + autoSend: boolean; // Auto-esegui azioni +} +``` + +**Game State Parser:** +```typescript +parseGameState(output: string): GameState { + // Estrae: location, exits, players, objects, description +} +``` + +**Decision Prompt:** +``` +Context: [ultimi 30 righe output] +Sei in: [location] +Uscite disponibili: north, south, east + +Cosa fai? (rispondi SOLO con il comando) +``` + +## Layer 2: Preload Script + + espone API sicure al renderer tramite `contextBridge`: + +```javascript +// Telnet +telnetConnect(host, port) +telnetSend(data) +onTelnetOutput(callback) + +// Ollama +ollamaChat(messages, options) +ollamaGetModels() +ollamaIsAvailable() + +// Autoplayer +autoplayerConfigure(config) +autoplayerStart() +autoplayerStop() +autoplayerExecuteAction(action) +onAutoplayerDecision(callback) +onAutoplayerAction(callback) +``` + +## Layer 3: React Renderer + +### App (`src/renderer/App.tsx`) + +Root component con navigation: + +```typescript +// Navigation state +const [activePanel, setActivePanel] = useState<'connection' | 'game' | 'autoplayer'>(); + +// Connection status +const [isConnected, setIsConnected] = useState(false); +``` + +### GameWindow (`src/renderer/components/GameWindow.tsx`) + +Display principale del gioco: + +| Componente | Funzione | +|------------|----------| +| Output div | Visualizza output HTML con colors | +| Form | Input comandi + submit | +| Auto-scroll | Mantiene focus su output recente | + +```typescript +// Subscribe a output Telnet +useEffect(() => { + window.electronAPI.onTelnetOutput((data) => { + setOutput(prev => prev + data); + }); +}, []); +``` + +### AutoplayerPanel (`src/renderer/components/AutoplayerPanel.tsx`) + +Pannello controllo autoplayer: + +| Feature | UI | +|---------|-----| +| Toggle On/Off | Switch button | +| Model selection | Dropdown (da Ollama) | +| System prompt | Textarea modificabile | +| Live status | Ultime decisioni/azioni | +| Ollama availability | Check automatico | + +## Flusso Dati + +### Telnet → UI +``` +1. Telnet server → telnet-client +2. TelnetService → emits 'output' +3. Main process → ipcRenderer.send('telnet-output') +4. GameWindow → onTelnetOutput callback +5. React state → setOutput() +6. Render → HTML display +``` + +### Autoplayer Decision +``` +1. Output Telnet arrives +2. AutoplayerService.processOutput() +3. parseGameState() → GameState { location, exits, ... } +4. shouldMakeDecision() → true/false +5. ollamaService.chat(messages) +6. extractAction() → "go north" +7. autoSend ? executeAction() : queueAction() +8. Events: 'decision', 'action' +9. UI updates: lastDecision, lastAction +``` + +## Configurazione + +### Environment +- Server Ollama: `http://ollama.solarpi.net` (hardcoded) +- LPMUD default: `172.24.19.225:6666` + +### Tipi Condivisi + +```typescript +// src/main/services/AutoplayerService.ts +interface GameState { + location: string; + description: string; + exits: string[]; + players: string[]; + objects: string[]; + health?: string; + mana?: string; + prompt?: string; +} + +interface OllamaMessage { + role: 'user' | 'assistant' | 'system'; + content: string; +} +``` + +## Limitazioni Attuali + +| Componente | Limitation | Piani | +|------------|------------|-------| +| ANSI Parsing | Solo colori basic | Parser completo | +| Game Parser | Regex-based, fragile | ML parser | +| Autoplayer | No memory persistent | SQLite storage | +| Mapper | Non implementato | Fase 3 | +| Triggers | Non implementati | Fase 3 | +| Plugins | Non implementati | Fase 4 | diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md new file mode 100644 index 0000000..0e2a411 --- /dev/null +++ b/docs/WORKFLOW.md @@ -0,0 +1,287 @@ +# Workflow di Sviluppo + +## Setup Ambiente + +### Prerequisiti +- Node.js 18+ +- npm 9+ +- Git + +### Installazione + +```bash +# Clone repository +cd mudlet-ai-client + +# Installa dipendenze +npm install + +# Verifica installazione +npm run dev # Vite dev server +npm start # Electron app +``` + +### Struttura Rami + +``` +main +├── develop # Branch di sviluppo attivo +├── feature/* # Feature branches +├── bugfix/* # Bug fix branches +└── release/* # Release branches +``` + +## Workflow Git + +### Feature Development + +```bash +# Crea branch da develop +git checkout develop +git checkout -b feature/nome-feature + +# Sviluppa e commit +git add . +git commit -m "feat: descrizione feature" + +# Push e PR +git push origin feature/nome-feature +# Crea Pull Request su GitHub +``` + +### Commit Convention + +``` +(): + +Types: +- feat: Nuova feature +- fix: Bug fix +- docs: Documentazione +- refactor: Code refactoring +- test: Aggiunta test +- chore: Manutenzione + +Examples: +feat(telnet): add ANSI color support +fix(autoplayer): resolve action queue ordering +docs(api): update IPC documentation +``` + +## Build & Test + +### Comandi NPM + +| Comando | Descrizione | +|---------|-------------| +| `npm run dev` | Avvia Vite dev server (port 3000) | +| `npm run build` | Build production (compila TS + Vite) | +| `npm run preview` | Preview build production | +| `npm start` | Avvia Electron app | + +### Build Production + +```bash +# Build completo +npm run build + +# Output in dist/ +dist/ +├── renderer/ # Bundle React +└── main/ # Bundle Electron main +``` + +### Debug + +```bash +# DevTools aperto automaticamente +npm start + +# Per debug remoto: +# 1. Apri DevTools (View → Toggle DevTools) +# 2. Console: renderer process +# 3. Terminale: main process (output console.log) +``` + +## Sviluppo Componenti + +### Aggiungere Nuovo Servizio Main Process + +1. Crea file in `src/main/services/NomeService.ts`: + +```typescript +import { EventEmitter } from 'events'; + +interface NomeServiceEvents { + 'event': [data: Type]; +} + +export class NomeService extends EventEmitter { + // Implementazione +} +``` + +2. Aggiungi in `src/main/index.ts`: + +```typescript +import { NomeService } from './services/NomeService'; + +const nomeService = new NomeService(); + +// IPC handlers +ipcMain.handle('nome-action', async (_, arg) => { + return nomeService.action(arg); +}); + +// Event forwarding +nomeService.on('event', (data) => { + if (mainWindow) { + mainWindow.webContents.send('nome-event', data); + } +}); +``` + +3. Aggiorna `src/preload/index.js`: + +```javascript +nomeAction: (arg) => ipcRenderer.invoke('nome-action', arg), +onNomeEvent: (callback) => ipcRenderer.on('nome-event', (_, data) => callback(data)), +``` + +### Aggiungere Componente UI + +1. Crea file in `src/renderer/components/NomeComponente.tsx`: + +```typescript +import { useState, useEffect } from 'react'; + +interface Props { + prop1: string; + onAction: (data: Type) => void; +} + +export function NomeComponente({ prop1, onAction }: Props) { + // Implementazione + return
...
; +} +``` + +2. Importa in `App.tsx`: + +```typescript +import { NomeComponente } from './components/NomeComponente'; + +// Usa nel render + +``` + +## IPC Communication Pattern + +### Request-Response (Renderer → Main) + +```typescript +// Renderer +const result = await window.electronAPI.someAction(arg); + +// Main +ipcMain.handle('some-action', async (_, arg) => { + return service.action(arg); +}); +``` + +### Events (Main → Renderer) + +```typescript +// Renderer +window.electronAPI.onSomeEvent((data) => { + console.log(data); +}); + +// Main +service.on('event', (data) => { + if (mainWindow) { + mainWindow.webContents.send('someEvent', data); + } +}); +``` + +### Two-Way Communication + +```typescript +// 1. Renderer invia richiesta +const action = await window.electronAPI.getDecision(context); + +// 2. Main processa +ipcMain.handle('getDecision', async (_, context) => { + return autoplayerService.decide(context); +}); + +// 3. Risultato tornato al renderer +``` + +## Testing + +### Test Manuali + +```bash +# 1. Avvia dev server +npm run dev + +# 2. Avvia Electron (nuovo terminale) +npm start + +# 3. Testa connessione +# - Host: 172.24.19.225 +# - Porta: 6666 + +# 4. Testa autoplayer +# - Attiva toggle +# - Verifica decisioni +``` + +### Checklist Pre-Release + +- [ ] TypeScript senza errori (`npm run build`) +- [ ] Componenti renderizzano correttamente +- [ ] Connessione Telnet funziona +- [ ] Ollama reachable e risponde +- [ ] Autoplayer genera azioni sensate +- [ ] No memory leaks (DevTools → Memory) +- [ ] UI responsive + +## Deployment + +### Electron Build + +```bash +# Installa electron-builder +npm install electron-builder --save-dev + +# Build per piattaforma corrente +npm run build +npx electron-builder + +# Output in release/ +``` + +### Update MUD_AUTOPLAYER_PLAN.md + +Dopo ogni fase completata: + +```markdown +### Fase N: Nome (DATA) - ✅ COMPLETATO +- [x] Task 1 +- [x] Task 2 +- [ ] Task 3 (futuro) +``` + +## Risorse + +| Risorsa | URL | +|---------|-----| +| Electron Docs | https://www.electronjs.org/docs | +| React Docs | https://react.dev | +| TypeScript | https://www.typescriptlang.org/docs | +| Vite | https://vitejs.dev | +| Tailwind | https://tailwindcss.com/docs | +| Ollama API | https://github.com/ollama/ollama/blob/main/docs/api.md | diff --git a/package.json b/package.json new file mode 100644 index 0000000..541b4ee --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "mudlet-ai-client", + "version": "1.0.0", + "description": "MUD Client with LLM Autoplayer", + "main": "dist/main/index.js", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "electron-vite preview", + "start": "electron ." + }, + "dependencies": { + "electron": "^28.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "telnet-client": "^2.0.3", + "ansicolor": "^2.0.1" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/node": "^20.10.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "electron-vite": "^1.0.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0", + "@vitejs/plugin-react": "^4.2.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..7509cc4 --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,112 @@ +import { app, BrowserWindow, ipcMain } from 'electron'; +import path from 'path'; +import { TelnetService } from './services/TelnetService'; +import { OllamaService } from './services/OllamaService'; +import { AutoplayerService } from './services/AutoplayerService'; + +let mainWindow: BrowserWindow | null = null; +const telnetService = new TelnetService(); +const ollamaService = new OllamaService(); +const autoplayerService = new AutoplayerService(ollamaService, telnetService); + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + mainWindow.loadURL('http://localhost:3000'); + mainWindow.webContents.openDevTools(); +} + +app.whenReady().then(createWindow); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +ipcMain.handle('telnet-connect', async (_, host: string, port: number) => { + return telnetService.connect(host, port); +}); + +ipcMain.handle('telnet-disconnect', async () => { + return telnetService.disconnect(); +}); + +ipcMain.handle('telnet-send', async (_, data: string) => { + return telnetService.send(data); +}); + +ipcMain.handle('telnet-get-output', async () => { + return telnetService.getOutput(); +}); + +telnetService.on('output', (data: string) => { + if (mainWindow) { + mainWindow.webContents.send('telnet-output', data); + } + autoplayerService.processOutput(data); +}); + +ipcMain.handle('ollama-chat', async (_, messages, options) => { + return ollamaService.chat(messages, options); +}); + +ipcMain.handle('ollama-get-models', async () => { + return ollamaService.getLocalModels(); +}); + +ipcMain.handle('ollama-is-available', async () => { + return ollamaService.isAvailable(); +}); + +ipcMain.handle('autoplayer-configure', async (_, config) => { + autoplayerService.configure(config); +}); + +ipcMain.handle('autoplayer-start', async () => { + autoplayerService.start(); +}); + +ipcMain.handle('autoplayer-stop', async () => { + autoplayerService.stop(); +}); + +ipcMain.handle('autoplayer-is-active', async () => { + return autoplayerService.isActive(); +}); + +ipcMain.handle('autoplayer-execute-action', async (_, action) => { + autoplayerService.executeAction(action); +}); + +autoplayerService.on('decision', (decision: string) => { + if (mainWindow) { + mainWindow.webContents.send('autoplayer-decision', decision); + } +}); + +autoplayerService.on('action', (action: string) => { + if (mainWindow) { + mainWindow.webContents.send('autoplayer-action', action); + } +}); + +autoplayerService.on('state-update', (state) => { + if (mainWindow) { + mainWindow.webContents.send('autoplayer-state', state); + } +}); diff --git a/src/main/services/AutoplayerService.ts b/src/main/services/AutoplayerService.ts new file mode 100644 index 0000000..0cf3683 --- /dev/null +++ b/src/main/services/AutoplayerService.ts @@ -0,0 +1,224 @@ +import { EventEmitter } from 'events'; +import { OllamaService } from './OllamaService'; +import { TelnetService } from './TelnetService'; + +interface GameState { + location: string; + description: string; + exits: string[]; + players: string[]; + objects: string[]; + health?: string; + mana?: string; + prompt?: string; +} + +interface AutoplayerConfig { + model: string; + systemPrompt: string; + temperature: number; + autoSend: boolean; +} + +interface AutoplayerEvents { + 'state-update': [GameState]; + 'action': [string]; + 'decision': [string]; + 'error': [Error]; +} + +export class AutoplayerService extends EventEmitter { + private ollamaService: OllamaService; + private telnetService: TelnetService; + private config: AutoplayerConfig; + private isRunning: boolean = false; + private lastOutput: string = ''; + private conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []; + private actionQueue: string[] = []; + + constructor(ollamaService: OllamaService, telnetService: TelnetService) { + super(); + this.ollamaService = ollamaService; + this.telnetService = telnetService; + this.config = { + model: 'llama3.2', + systemPrompt: this.getDefaultSystemPrompt(), + temperature: 0.7, + autoSend: false, + }; + } + + private getDefaultSystemPrompt(): string { + return `Sei un giocatore esperto di MUD (Multi-User Dungeon). +Il tuo obiettivo è esplorare, interagire con l'ambiente e altri giocatori in modo naturale. + +Regole: +1. Analizza SEMPRE il contesto prima di agire +2. I tuoi comandi devono essere coerenti con la situazione +3. Rispondi in modo appropriato se qualcuno ti parla +4. Non fare azioni ripetitive senza motivo +5. Mantieni un comportamento naturale e coerente + +Quando devi agire, rispondi SOLO con il comando da eseguire, niente altro. +Per più comandi, separali con punto e virgola. +Esempi: "look", "go north", "say Ciao a tutti!", "get sword"`; + } + + configure(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + start(): void { + if (this.isRunning) return; + this.isRunning = true; + this.conversationHistory = []; + } + + stop(): void { + this.isRunning = false; + this.ollamaService.abort(); + } + + isActive(): boolean { + return this.isRunning; + } + + processOutput(output: string): void { + this.lastOutput = output; + const gameState = this.parseGameState(output); + + if (gameState.location) { + this.emit('state-update', gameState); + } + + if (this.isRunning && this.shouldMakeDecision(output)) { + this.makeDecision(gameState, output); + } + } + + private parseGameState(output: string): GameState { + const state: GameState = { + location: '', + description: '', + exits: [], + players: [], + objects: [], + }; + + const locationMatch = output.match(/(?:Sei in|Ti trovi in|In)\s+([^\n\r.]+)/i); + if (locationMatch) { + state.location = locationMatch[1].trim(); + } + + const exitMatch = output.match(/Uscite?:?\s*([^\n\r]+)/i); + if (exitMatch) { + state.exits = exitMatch[1] + .split(/[,;]/) + .map((e) => e.trim().toLowerCase()) + .filter((e) => e); + } + + const playersMatch = output.match(>([^<]+)/g); + if (playersMatch) { + state.players = playersMatch.map((p) => p.replace('>', '').trim()); + } + + return state; + } + + private shouldMakeDecision(output: string): boolean { + const promptPatterns = [ + />\s*$/m, + /Comando:\s*$/i, + /Cosa vuoi fare\?/i, + /\.\.\.\s*$/, + ]; + + return promptPatterns.some((pattern) => pattern.test(output)) || + output.includes('Dice qualcosa a gran voce:') || + output.length > 500; + } + + private async makeDecision(gameState: GameState, context: string): Promise { + const recentContext = this.getRecentContext(context); + + const messages = [ + { role: 'system' as const, content: this.config.systemPrompt }, + ...this.conversationHistory.slice(-10), + { role: 'user' as const, content: this.buildDecisionPrompt(gameState, recentContext) }, + ]; + + try { + const response = await this.ollamaService.chat(messages, { + model: this.config.model, + temperature: this.config.temperature, + }); + + const action = this.extractAction(response.response); + this.emit('decision', response.response); + this.emit('action', action); + + this.conversationHistory.push( + { role: 'user', content: recentContext }, + { role: 'assistant', content: action } + ); + + if (this.config.autoSend) { + this.executeAction(action); + } else { + this.actionQueue.push(action); + } + } catch (error) { + this.emit('error', error as Error); + } + } + + private getRecentContext(context: string): string { + const lines = context.split('\n').slice(-30); + return lines.join('\n'); + } + + private buildDecisionPrompt(gameState: GameState, context: string): string { + let prompt = `Context:\n${context}\n\n`; + + if (gameState.location) { + prompt += `Sei in: ${gameState.location}\n`; + } + if (gameState.exits.length > 0) { + prompt += `Uscite disponibili: ${gameState.exits.join(', ')}\n`; + } + + prompt += `\nCosa fai? (rispondi SOLO con il comando)`; + return prompt; + } + + private extractAction(response: string): string { + const cleaned = response + .replace(/^[\s\n]*(?:Azione:|Action:|Comando:)\s*/i, '') + .replace(/\n.*/s, '') + .trim(); + + const match = cleaned.match(/^["']?([^"';]+)["']?;?$/); + return match ? match[1].trim() : cleaned; + } + + getNextAction(): string | null { + return this.actionQueue.shift() || null; + } + + executeAction(action: string): void { + this.telnetService.send(action + '\n'); + } + + queueAction(action: string): void { + this.actionQueue.push(action); + } + + clearActionQueue(): void { + this.actionQueue = []; + } + + getPendingActions(): string[] { + return [...this.actionQueue]; + } +} diff --git a/src/main/services/OllamaService.ts b/src/main/services/OllamaService.ts new file mode 100644 index 0000000..772159b --- /dev/null +++ b/src/main/services/OllamaService.ts @@ -0,0 +1,187 @@ +import { EventEmitter } from 'events'; + +interface OllamaMessage { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +interface OllamaRequest { + model: string; + messages: OllamaMessage[]; + stream?: boolean; + options?: { + temperature?: number; + top_p?: number; + num_predict?: number; + }; +} + +interface OllamaResponse { + model: string; + response: string; + done: boolean; +} + +interface OllamaServiceEvents { + 'stream': [string]; + 'complete': [OllamaResponse]; + 'error': [Error]; +} + +export class OllamaService extends EventEmitter { + private baseUrl: string = 'http://ollama.solarpi.net'; + private currentModel: string = 'llama3.2'; + private abortController: AbortController | null = null; + + setBaseUrl(url: string): void { + this.baseUrl = url; + } + + setModel(model: string): void { + this.currentModel = model; + } + + getModels(): string[] { + return [ + 'llama3.2', + 'llama3.1', + 'mistral', + 'codellama', + 'llama3', + 'phi3', + 'gemma', + ]; + } + + async chat(messages: OllamaMessage[], options?: { model?: string; temperature?: number }): Promise { + const model = options?.model || this.currentModel; + this.abortController = new AbortController(); + + const request: OllamaRequest = { + model, + messages, + stream: false, + options: { + temperature: options?.temperature ?? 0.7, + num_predict: 512, + }, + }; + + try { + const response = await fetch(`${this.baseUrl}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + signal: this.abortController.signal, + }); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + + const data = await response.json(); + const result: OllamaResponse = { + model: data.model, + response: data.message?.content || data.response || '', + done: true, + }; + + this.emit('complete', result); + return result; + } catch (error) { + this.emit('error', error as Error); + throw error; + } + } + + async *chatStream(messages: OllamaMessage[], options?: { model?: string }): AsyncGenerator { + const model = options?.model || this.currentModel; + this.abortController = new AbortController(); + + const request: OllamaRequest = { + model, + messages, + stream: true, + }; + + try { + const response = await fetch(`${this.baseUrl}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + signal: this.abortController.signal, + }); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + + const reader = response.body?.getReader(); + if (!reader) throw new Error('No response body'); + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + try { + const data = JSON.parse(line); + if (data.message?.content) { + const chunk = data.message.content; + this.emit('stream', chunk); + yield chunk; + } + if (data.done) break; + } catch { + // Skip invalid JSON lines + } + } + } + } catch (error) { + this.abort(); + this.emit('error', error as Error); + throw error; + } + } + + abort(): void { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + } + + async isAvailable(): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/tags`, { + method: 'GET', + timeout: 5000, + }); + return response.ok; + } catch { + return false; + } + } + + async getLocalModels(): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/tags`, { + method: 'GET', + timeout: 5000, + }); + if (!response.ok) return []; + const data = await response.json(); + return data.models?.map((m: { name: string }) => m.name.replace(':', '-')) || []; + } catch { + return []; + } + } +} diff --git a/src/main/services/TelnetService.ts b/src/main/services/TelnetService.ts new file mode 100644 index 0000000..29760e8 --- /dev/null +++ b/src/main/services/TelnetService.ts @@ -0,0 +1,129 @@ +import { EventEmitter } from 'events'; +import { Connection, ConnectionOptions } from 'telnet-client'; +import ansicolor from 'ansicolor'; + +interface TelnetServiceEvents { + output: [string]; + connected: []; + disconnected: []; + error: [Error]; +} + +export class TelnetService extends EventEmitter { + private connection: Connection | null = null; + private outputBuffer: string = ''; + private isConnected: boolean = false; + + async connect(host: string, port: number): Promise { + try { + this.connection = new Connection(); + + const options: ConnectionOptions = { + host, + port, + timeout: 5000, + shellPrompt: />|#|:/, + crlf: '\r\n', + recvBufferSize: 1024, + }; + + await this.connection.connect(options); + this.isConnected = true; + + this.startOutputListener(); + + this.emit('connected'); + return true; + } catch (error) { + this.emit('error', error as Error); + return false; + } + } + + async disconnect(): Promise { + if (this.connection) { + await this.connection.end(); + this.connection = null; + this.isConnected = false; + this.emit('disconnected'); + } + } + + async send(data: string): Promise { + if (!this.connection || !this.isConnected) { + return false; + } + + try { + await this.connection.send(data, { timeout: 3000 }); + return true; + } catch (error) { + this.emit('error', error as Error); + return false; + } + } + + getOutput(): string { + return this.outputBuffer; + } + + clearOutput(): void { + this.outputBuffer = ''; + } + + private async startOutputListener(): Promise { + if (!this.connection) return; + + const processOutput = async () => { + try { + const output = await this.connection!.receive(); + if (output) { + const cleanedOutput = this.parseAnsiCodes(output); + this.outputBuffer += cleanedOutput; + this.emit('output', cleanedOutput); + } + } catch { + if (this.isConnected) { + setTimeout(processOutput, 100); + } + } + }; + + processOutput(); + } + + private parseAnsiCodes(text: string): string { + const parsed = ansicolor.parse(text); + + return parsed.spans + .map((span) => { + let result = span.text; + + if (span.color) { + if (typeof span.color === 'object') { + if ('r' in span.color) { + result = `${result}`; + } else { + result = `${result}`; + } + } else { + result = `${result}`; + } + } + + if (span.bgColor) { + const bgColor = typeof span.bgColor === 'object' && 'r' in span.bgColor + ? `rgb(${span.bgColor.r},${span.bgColor.g},${span.bgColor.b})` + : span.bgColor.name; + result = `${result}`; + } + + if (span.css) { + result = `${result}`; + } + + return result; + }) + .join(''); + } +} diff --git a/src/preload/index.js b/src/preload/index.js new file mode 100644 index 0000000..c4a7d3a --- /dev/null +++ b/src/preload/index.js @@ -0,0 +1,21 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +contextBridge.exposeInMainWorld('electronAPI', { + telnetConnect: (host, port) => ipcRenderer.invoke('telnet-connect', host, port), + telnetDisconnect: () => ipcRenderer.invoke('telnet-disconnect'), + telnetSend: (data) => ipcRenderer.invoke('telnet-send', data), + telnetGetOutput: () => ipcRenderer.invoke('telnet-get-output'), + onTelnetOutput: (callback) => ipcRenderer.on('telnet-output', (_, data) => callback(data)), + ollamaChat: (messages, options) => ipcRenderer.invoke('ollama-chat', messages, options), + ollamaGetModels: () => ipcRenderer.invoke('ollama-get-models'), + ollamaIsAvailable: () => ipcRenderer.invoke('ollama-is-available'), + autoplayerConfigure: (config) => ipcRenderer.invoke('autoplayer-configure', config), + autoplayerStart: () => ipcRenderer.invoke('autoplayer-start'), + autoplayerStop: () => ipcRenderer.invoke('autoplayer-stop'), + autoplayerIsActive: () => ipcRenderer.invoke('autoplayer-is-active'), + autoplayerExecuteAction: (action) => ipcRenderer.invoke('autoplayer-execute-action', action), + onAutoplayerDecision: (callback) => ipcRenderer.on('autoplayer-decision', (_, data) => callback(data)), + onAutoplayerAction: (callback) => ipcRenderer.on('autoplayer-action', (_, data) => callback(data)), + onAutoplayerState: (callback) => ipcRenderer.on('autoplayer-state', (_, data) => callback(data)), +}); + diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 0000000..9e70c09 --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { GameWindow } from './components/GameWindow'; +import { ConnectionPanel } from './components/ConnectionPanel'; +import { AutoplayerPanel } from './components/AutoplayerPanel'; + +export default function App() { + const [isConnected, setIsConnected] = useState(false); + const [activePanel, setActivePanel] = useState<'connection' | 'game' | 'autoplayer'>('connection'); + + return ( +
+
+

MUDlet AI Client

+
+ + + +
+
+
+ +
+ {activePanel === 'connection' && setIsConnected(true)} />} + {activePanel === 'game' && } + {activePanel === 'autoplayer' && } +
+
+ ); +} diff --git a/src/renderer/components/AutoplayerPanel.tsx b/src/renderer/components/AutoplayerPanel.tsx new file mode 100644 index 0000000..334c4cf --- /dev/null +++ b/src/renderer/components/AutoplayerPanel.tsx @@ -0,0 +1,148 @@ +import { useState, useEffect, useCallback } from 'react'; + +export function AutoplayerPanel() { + const [enabled, setEnabled] = useState(false); + const [model, setModel] = useState('llama3.2'); + const [models, setModels] = useState(['llama3.2', 'llama3.1', 'mistral', 'codellama']); + const [systemPrompt, setSystemPrompt] = useState( + 'Sei un giocatore esperto di MUD. Analizza sempre il contesto prima di agire. ' + + 'Rispondi in modo coerente con la situazione. I tuoi comandi devono essere realistici per un MUD fantasy.' + ); + const [lastDecision, setLastDecision] = useState(''); + const [lastAction, setLastAction] = useState(''); + const [ollamaAvailable, setOllamaAvailable] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + checkOllamaAvailability(); + loadModels(); + + if (window.electronAPI) { + window.electronAPI.onAutoplayerDecision((decision: string) => { + setLastDecision(decision); + }); + window.electronAPI.onAutoplayerAction((action: string) => { + setLastAction(action); + }); + } + }, []); + + const checkOllamaAvailability = async () => { + if (window.electronAPI) { + const available = await window.electronAPI.ollamaIsAvailable(); + setOllamaAvailable(available); + } + }; + + const loadModels = async () => { + if (window.electronAPI) { + const localModels = await window.electronAPI.ollamaGetModels(); + if (localModels.length > 0) { + setModels(localModels); + } + } + }; + + const handleToggle = useCallback(async () => { + if (!window.electronAPI) return; + + setLoading(true); + try { + if (enabled) { + await window.electronAPI.autoplayerStop(); + } else { + await window.electronAPI.autoplayerConfigure({ model, systemPrompt }); + await window.electronAPI.autoplayerStart(); + } + setEnabled(!enabled); + } finally { + setLoading(false); + } + }, [enabled, model, systemPrompt]); + + const handleModelChange = async (newModel: string) => { + setModel(newModel); + if (enabled && window.electronAPI) { + await window.electronAPI.autoplayerConfigure({ model: newModel }); + } + }; + + const handlePromptChange = async (newPrompt: string) => { + setSystemPrompt(newPrompt); + if (enabled && window.electronAPI) { + await window.electronAPI.autoplayerConfigure({ systemPrompt: newPrompt }); + } + }; + + return ( +
+

Autoplayer Settings

+ +
+
+
+ Autoplayer Attivo + {!ollamaAvailable && ( +

Ollama non disponibile

+ )} +
+ +
+ +
+ + +
+ +
+ +