ollama-code-qwen
Version:
Un assistant IA en ligne de commande utilisant Ollama et le modèle qwen2.5-coder pour aider au développement, avec des capacités MCP améliorées et détection d'intentions en français et anglais
573 lines (515 loc) • 15.2 kB
JavaScript
/**
* Module pour l'interface utilisateur texte (TUI)
*/
import blessed from 'blessed';
import { CustomInput } from './custom-input.js';
import TerminalRenderer from 'marked-terminal';
import { marked } from 'marked';
import chalk from 'chalk';
import { createRequire } from 'module';
// Pour assurer la compatibilité avec toutes les versions de Node
const require = createRequire(import.meta.url);
// Désactiver les avertissements de blessed
process.env.BLESSED_FORCE_MODES = '1';
process.env.TERM = 'xterm-256color';
// Configuration du renderer pour Markdown
marked.setOptions({
renderer: new TerminalRenderer({
code: chalk.cyan,
blockquote: chalk.gray.italic,
table: chalk.white,
listitem: chalk.yellow,
strong: chalk.bold.green,
em: chalk.italic.cyan,
heading: chalk.bold.blueBright,
codespan: chalk.magenta,
hr: chalk.gray,
tab: 2,
tableOptions: {
chars: {
'top': '\u2550', 'top-mid': '\u2564', 'top-left': '\u2554', 'top-right': '\u2557',
'bottom': '\u2550', 'bottom-mid': '\u2567', 'bottom-left': '\u255a', 'bottom-right': '\u255d',
'left': '\u2551', 'left-mid': '\u255f', 'mid': '\u2500', 'mid-mid': '\u253c',
'right': '\u2551', 'right-mid': '\u2562', 'middle': '\u2502'
}
}
})
});
export class UIManager {
constructor(options = {}) {
this.screen = blessed.screen({
smartCSR: true,
title: 'Ollama Code',
fullUnicode: true,
dockBorders: true,
warnings: false
});
// Configuration
this.model = options.model || 'qwen2.5-coder:14b';
this.host = options.host || 'http://192.168.1.16:11434';
this.version = options.version || '0.1.0';
// Créer les composants de l'interface
this._createComponents();
// Configurer les événements clavier
this._setupKeyBindings();
// Historique des messages
this.history = [];
this.historyIndex = -1;
// Callbacks
this.onSubmit = options.onSubmit || (() => {});
this.onCommand = options.onCommand || (() => {});
this.onExit = options.onExit || (() => {});
// État de l'interface
this.loading = false;
this.focusInput();
}
/**
* Crée les composants de l'interface utilisateur
*/
_createComponents() {
// Définir la taille des marges selon la taille du terminal
let inputHeight = 3;
if (this.screen.height < 24) {
inputHeight = 3;
} else {
inputHeight = Math.max(3, Math.floor(this.screen.height * 0.1));
}
// En-tête - affiche le logo et les informations
this.header = blessed.box({
top: 0,
left: 0,
width: '100%',
height: 3,
content: this._formatHeader(),
style: {
fg: 'white',
bg: 'green',
},
tags: true,
});
// Zone de conversation - affiche les messages
this.chatBox = blessed.scrollablebox({
top: 3,
left: 0,
width: '100%',
height: `100%-${inputHeight+4}`,
scrollable: true,
mouse: true,
keys: true,
vi: true,
alwaysScroll: true,
tags: true,
scrollbar: {
ch: ' ',
style: {
bg: 'gray',
},
},
style: {
fg: 'white',
},
});
// Zone de status - affiche les infos sur le contexte actuel
this.statusBar = blessed.box({
bottom: inputHeight+1,
left: 0,
width: '100%',
height: 1,
content: ' Context: Current Project | Model: qwen2.5-coder:14b',
style: {
fg: 'black',
bg: 'white',
},
tags: true,
});
// Barre d'aide - affiche les raccourcis clavier
this.helpBar = blessed.box({
bottom: inputHeight,
left: 0,
width: '100%',
height: 1,
content: ' ^C: Exit | ^R: Refresh | ^L: Clear | ^G: Git | ^S: Save',
style: {
fg: 'white',
bg: 'blue',
},
tags: true,
});
// Zone de saisie - pour entrer les commandes et questions
// Ajouter le callback onSubmit pour gérer la soumission
this.customInput = new CustomInput({
screen: this.screen,
top: this.screen.height - inputHeight,
left: 0,
width: '100%',
height: inputHeight,
style: {
fg: 'white',
bg: 'black',
border: {
fg: 'blue',
type: 'line'
},
focus: {
bg: 'black',
},
},
onSubmit: (text) => {
if (text.trim()) {
// Traiter les doublons si nécessaire
if (this._hasRepeatingChars(text)) {
text = this._fixDuplicateChars(text);
}
this._submitInput(text);
}
}
});
this.inputBox = this.customInput.getDisplay();
// Indicateur de chargement
this.loader = blessed.loading({
parent: this.screen,
top: 'center',
left: 'center',
width: 30,
height: 5,
border: {
type: 'line',
},
style: {
fg: 'blue',
bg: 'black',
border: {
fg: 'blue',
},
},
});
// Ajouter les composants à l'écran
this.screen.append(this.header);
this.screen.append(this.chatBox);
this.screen.append(this.statusBar);
this.screen.append(this.helpBar);
this.screen.append(this.inputBox);
// Gérer le redimensionnement de la fenêtre
this.screen.on('resize', () => {
// Réajuster les composants si nécessaire
this.chatBox.height = `100%-${inputHeight+4}`;
this.header.width = this.screen.width;
this.chatBox.width = this.screen.width;
this.statusBar.width = this.screen.width;
this.statusBar.bottom = inputHeight+1;
this.helpBar.width = this.screen.width;
this.helpBar.bottom = inputHeight;
this.customInput.getDisplay().width = this.screen.width;
this.customInput.getDisplay().top = this.screen.height - inputHeight;
// Actualiser l'affichage
this.screen.render();
});
}
/**
* Configure les raccourcis clavier
*/
_setupKeyBindings() {
// Configurer la touche Escape pour quitter
this.screen.key(['escape', 'C-c'], () => {
this.onExit();
process.exit(0);
});
// Touches pour les commandes spéciales
this.screen.key('C-l', () => this.clearChat());
this.screen.key('C-r', () => this.handleCommand('/refresh'));
this.screen.key('C-g', () => this.showGitMenu());
}
/**
* Soumet l'entrée utilisateur pour traitement
* @param {string} text - Texte saisi
*/
_submitInput(text) {
const trimmedText = text.trim();
if (!trimmedText) return;
// Nettoyer l'input et actualiser l'affichage
this.inputBox.setValue('');
this.screen.render();
// Ajouter à l'historique du composant personnalisé
this.customInput.addToHistory(trimmedText);
// Traiter la commande ou l'entrée normale
if (trimmedText.startsWith('/')) {
this.handleCommand(trimmedText);
} else {
this.handleUserInput(trimmedText);
}
}
/**
* Vérifie si le texte contient des caractères répétés (doublés)
* @param {string} text - Texte à vérifier
* @returns {boolean} - True si les caractères semblent être doublés
*/
_hasRepeatingChars(text) {
if (!text || text.length < 6) return false;
// Vérifie un échantillon du texte pour détecter les modèles de répétition
const sampleSize = Math.min(10, Math.floor(text.length / 2));
let repeatingCount = 0;
for (let i = 0; i < text.length - 1; i += 2) {
if (i + 1 < text.length && text[i] === text[i + 1]) {
repeatingCount++;
}
}
const repeatingRatio = repeatingCount / Math.floor(text.length / 2);
return repeatingRatio > 0.6; // Si plus de 60% des paires sont des doublons
}
/**
* Corrige les caractères doublés dans l'entrée utilisateur
* @param {string} text - Texte avec caractères potentiellement doublés
* @returns {string} - Texte corrigé
*/
_fixDuplicateChars(text) {
if (!text) return '';
let result = '';
for (let i = 0; i < text.length; i += 2) {
result += text[i];
// Si nous sommes au dernier caractère et que la longueur est impaire
if (i + 1 === text.length - 1 && text.length % 2 !== 0) {
result += text[i + 1];
}
}
return result;
}
/**
* Formate l'en-tête de l'application
*/
_formatHeader() {
return ` Ollama Code ${this.version} - Connected to ${this.host} - Model: ${this.model}`;
}
/**
* Gère l'entrée utilisateur
*/
handleUserInput(text) {
// Ajouter au début de l'historique interne
if (this.history.indexOf(text) === -1) {
this.history.unshift(text);
if (this.history.length > 50) this.history.pop();
}
this.historyIndex = -1;
// Afficher le message utilisateur
this.addUserMessage(text);
// Appeler le callback pour traiter l'entrée
this.onSubmit(text);
}
/**
* Gère les commandes spéciales
*/
handleCommand(command) {
// Ajouter au début de l'historique interne et du composant
if (this.history.indexOf(command) === -1) {
this.history.unshift(command);
if (this.history.length > 50) this.history.pop();
}
this.historyIndex = -1;
this.customInput.addToHistory(command);
// Afficher la commande
this.addSystemMessage(`Executing command: ${command}`);
// Appeler le callback pour traiter la commande
this.onCommand(command);
}
/**
* Affiche un menu Git
*/
showGitMenu() {
const menu = blessed.list({
parent: this.screen,
top: 'center',
left: 'center',
width: 40,
height: 12,
border: {
type: 'line'
},
style: {
fg: 'white',
bg: 'black',
border: {
fg: 'green'
},
selected: {
bg: 'green',
fg: 'black'
}
},
label: ' Git Operations ',
items: [
'Status',
'Stage all changes',
'Commit staged changes',
'Pull from remote',
'Push to remote',
'View commit history',
'Create new branch',
'Switch branch',
'Cancel'
]
});
menu.on('select', (item, idx) => {
menu.detach();
this.screen.render();
switch(idx) {
case 0: // Status
this.handleCommand('/git status');
break;
case 1: // Stage all
this.handleCommand('/git add .');
break;
case 2: // Commit
this.promptForInput('Enter commit message:', (message) => {
this.handleCommand(`/git commit -m "${message}"`);
});
break;
case 3: // Pull
this.handleCommand('/git pull');
break;
case 4: // Push
this.handleCommand('/git push');
break;
case 5: // History
this.handleCommand('/git log');
break;
case 6: // New branch
this.promptForInput('Enter new branch name:', (branch) => {
this.handleCommand(`/git checkout -b ${branch}`);
});
break;
case 7: // Switch branch
this.handleCommand('/git branch');
// Idéalement on voudrait lister les branches et permettre d'en sélectionner une
break;
case 8: // Cancel
// Ne rien faire
break;
}
});
menu.focus();
this.screen.render();
}
/**
* Affiche une boîte de dialogue pour saisir du texte
*/
promptForInput(prompt, callback) {
const promptBox = blessed.prompt({
parent: this.screen,
top: 'center',
left: 'center',
width: 60,
height: 8,
border: 'line',
style: {
fg: 'white',
bg: 'black',
border: {
fg: 'green'
}
}
});
promptBox.input(prompt, '', (err, value) => {
if (!err && value) {
callback(value);
}
this.screen.render();
});
}
/**
* Ajoute un message utilisateur à la conversation
*/
addUserMessage(text) {
const formattedMessage = `\n{bold}{cyan}You:{/cyan}{/bold}\n${text}\n`;
const content = this.chatBox.getContent() || '';
this.chatBox.setContent(content + blessed.parseTags(formattedMessage));
this.chatBox.setScrollPerc(100);
this.screen.render();
}
/**
* Ajoute un message du système à la conversation
*/
addSystemMessage(text) {
const formattedMessage = `\n{bold}{yellow}System:{/yellow}{/bold}\n${text}\n`;
const content = this.chatBox.getContent() || '';
this.chatBox.setContent(content + blessed.parseTags(formattedMessage));
this.chatBox.setScrollPerc(100);
this.screen.render();
}
/**
* Ajoute un message de l'assistant à la conversation
*/
addAssistantMessage(text) {
try {
// Convertir le markdown en texte formaté
const formattedMarkdown = marked(text);
const formattedMessage = `\n{bold}{green}AI Assistant:{/green}{/bold}\n${formattedMarkdown}\n`;
const content = this.chatBox.getContent() || '';
this.chatBox.setContent(content + formattedMessage);
this.chatBox.setScrollPerc(100);
this.screen.render();
} catch (error) {
// En cas d'erreur, afficher le texte brut
this.addSystemMessage(`Error formatting message: ${error.message}`);
const content = this.chatBox.getContent() || '';
this.chatBox.setContent(content + `\n{bold}{green}AI Assistant:{/green}{/bold}\n${text}\n`);
this.chatBox.setScrollPerc(100);
this.screen.render();
}
}
/**
* Affiche un message d'erreur
*/
showError(text) {
const formattedMessage = `\n{bold}{red}Error:{/red}{/bold}\n${text}\n`;
const content = this.chatBox.getContent() || '';
this.chatBox.setContent(content + blessed.parseTags(formattedMessage));
this.chatBox.setScrollPerc(100);
this.screen.render();
}
/**
* Efface la conversation
*/
clearChat() {
this.chatBox.setContent('');
this.addSystemMessage('Chat cleared. Context is preserved.');
this.screen.render();
}
/**
* Met à jour la barre d'état
*/
updateStatus(text) {
this.statusBar.setContent(` ${text}`);
this.screen.render();
}
/**
* Démarre l'indicateur de chargement
*/
startLoading(message = 'Thinking...') {
this.loading = true;
this.loader.load(message);
this.screen.render();
}
/**
* Arrête l'indicateur de chargement
*/
stopLoading() {
this.loading = false;
this.loader.stop();
this.screen.render();
}
/**
* Place le focus sur la zone de saisie
*/
focusInput() {
this.customInput.focus();
this.screen.render();
}
/**
* Initialise et démarre l'interface
*/
start() {
this.screen.render();
this.focusInput();
this.addSystemMessage('Welcome to Ollama Code! Type your question or use / for commands.');
this.addSystemMessage('Use Ctrl+G to access Git operations or type /help for more commands.');
}
}