UNPKG

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
/** * 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.'); } }