Creacion de la AEditor Basicamente un Editor solo para el bot

feat: Add new commands for Discord bot

- Implemented "Everyone" command that replies to the user when executed.
- Added "sdfsdfsdf" command with an alias "dfsf" that also replies to the user.
- Enhanced the command structure with type definitions for better type safety.
This commit is contained in:
Shni
2025-11-04 03:09:23 -06:00
parent d5048dda38
commit 954e0ba333
67 changed files with 20863 additions and 12 deletions

722
AEditor/src/App.vue Normal file
View File

@@ -0,0 +1,722 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
import Sidebar from "./components/Sidebar.vue";
import MonacoEditor from "./components/MonacoEditor.vue";
import CommandCreator from "./components/CommandCreator.vue";
import EventCreator from "./components/EventCreator.vue";
import ProjectSelector from "./components/ProjectSelector.vue";
import CommandPalette from "./components/CommandPalette.vue";
import SkeletonLoader from "./components/SkeletonLoader.vue";
import DatabaseViewer from "./components/DatabaseViewer.vue";
import EnvManager from "./components/EnvManager.vue";
import type { ProjectStats, FileInfo, Command, Event } from "./types/bot";
// Estado de la aplicación
const projectRoot = ref<string>("");
const showProjectSelector = ref(false);
const showCommandPalette = ref(false);
const devUltraMode = ref(false);
const initialLoading = ref(true);
const stats = ref<ProjectStats>({
messageCommands: 0,
slashCommands: 0,
standardEvents: 0,
customEvents: 0,
totalCommands: 0,
totalEvents: 0,
});
const commands = ref<FileInfo[]>([]);
const events = ref<FileInfo[]>([]);
const allFiles = ref<FileInfo[]>([]);
const selectedFile = ref<FileInfo | null>(null);
const fileContent = ref<string>("");
const currentView = ref<"editor" | "command-creator" | "event-creator" | "database" | "env-manager">("editor");
const loading = ref(false);
const errorMsg = ref<string>("");
const schemaContent = ref<string>("");
// Deshabilitar F12 y habilitar Ctrl+Q
const handleF12 = (e: KeyboardEvent) => {
if (e.key === 'F12') {
e.preventDefault();
e.stopPropagation();
return false;
}
};
const handleCtrlQ = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'q') {
e.preventDefault();
e.stopPropagation();
showCommandPalette.value = !showCommandPalette.value;
}
};
onMounted(() => {
window.addEventListener('keydown', handleF12);
window.addEventListener('keydown', handleCtrlQ);
// Inicializar Discord RPC
initDiscordRPC();
// Cargar proyecto inicial
loadProjectData();
});
onUnmounted(() => {
window.removeEventListener('keydown', handleF12);
window.removeEventListener('keydown', handleCtrlQ);
// Desconectar Discord RPC al cerrar
disconnectDiscordRPC();
});
// Funciones Discord RPC
async function initDiscordRPC() {
try {
await invoke('init_discord_rpc');
console.log('✅ Discord RPC inicializado');
} catch (error) {
console.warn('⚠️ No se pudo inicializar Discord RPC:', error);
}
}
async function updateDiscordRPC(details: string, state: string, fileName?: string) {
try {
await invoke('update_discord_rpc', { details, state, fileName });
} catch (error) {
console.warn('⚠️ Error actualizando Discord RPC:', error);
}
}
async function disconnectDiscordRPC() {
try {
await invoke('disconnect_discord_rpc');
} catch (error) {
console.warn('⚠️ Error desconectando Discord RPC:', error);
}
}
// Cargar datos del proyecto
async function loadProjectData() {
loading.value = true;
errorMsg.value = "";
try {
// Si no hay projectRoot, intentar cargar desde localStorage o mostrar selector
if (!projectRoot.value) {
const savedPath = localStorage.getItem('amayo-project-path');
if (savedPath) {
// Validar ruta guardada
const isValid = await invoke<boolean>('validate_project_path', {
path: savedPath,
});
if (isValid) {
projectRoot.value = savedPath;
} else {
// Ruta inválida, mostrar selector
showProjectSelector.value = true;
initialLoading.value = false;
loading.value = false;
return;
}
} else {
// No hay ruta guardada, mostrar selector
showProjectSelector.value = true;
initialLoading.value = false;
loading.value = false;
return;
}
}
// Cargar estadísticas
stats.value = await invoke<ProjectStats>("get_project_stats", {
projectRoot: projectRoot.value
});
console.log("📊 Stats cargadas:", stats.value);
// Cargar comandos
commands.value = await invoke<FileInfo[]>("scan_commands", {
projectRoot: projectRoot.value
});
console.log("💬 Comandos cargados:", commands.value.length, commands.value);
// Cargar eventos
events.value = await invoke<FileInfo[]>("scan_events", {
projectRoot: projectRoot.value
});
console.log("📡 Eventos cargados:", events.value.length, events.value);
// Cargar schema.prisma si existe
try {
schemaContent.value = await invoke<string>("read_file_content", {
filePath: `${projectRoot.value}/prisma/schema.prisma`
});
} catch {
schemaContent.value = "// Schema no encontrado";
}
// Actualizar Discord RPC
updateDiscordRPC(
"Navegando proyecto",
`${stats.value.totalCommands} comandos | ${stats.value.totalEvents} eventos`
);
} catch (error: any) {
errorMsg.value = `Error cargando proyecto: ${error}`;
console.error("Error:", error);
} finally {
loading.value = false;
// Pequeño delay para mostrar el skeleton
setTimeout(() => {
initialLoading.value = false;
}, 800);
}
}
// Seleccionar archivo
async function selectFile(file: FileInfo) {
try {
selectedFile.value = file;
fileContent.value = await invoke<string>("read_file_content", {
filePath: file.path
});
currentView.value = "editor";
// Actualizar Discord RPC
const fileType = file.commandType
? `Comando ${file.commandType}`
: file.eventType
? `Evento ${file.eventType}`
: "Archivo";
updateDiscordRPC(`Editando ${fileType}`, file.name);
} catch (error: any) {
errorMsg.value = `Error leyendo archivo: ${error}`;
console.error("Error:", error);
}
}
// Guardar archivo
async function saveFile(content: string) {
if (!selectedFile.value) return;
try {
await invoke("write_file_content", {
filePath: selectedFile.value.path,
content: content
});
// Mostrar notificación de éxito
showNotification("✅ Archivo guardado correctamente");
// Recargar estadísticas
await loadProjectData();
} catch (error: any) {
errorMsg.value = `Error guardando archivo: ${error}`;
console.error("Error:", error);
}
}
// Crear nuevo comando
function showCommandCreator() {
currentView.value = "command-creator";
updateDiscordRPC("Creando comando nuevo", "En el wizard de comandos");
}
// Crear nuevo evento
function showEventCreator() {
currentView.value = "event-creator";
updateDiscordRPC("Creando evento nuevo", "En el wizard de eventos");
}
// Guardar nuevo comando
async function saveCommand(_command: Command, code: string, savePath: string) {
try {
const fullPath = `${projectRoot.value}/${savePath}`;
await invoke("write_file_content", {
filePath: fullPath,
content: code
});
showNotification("✅ Comando creado correctamente");
currentView.value = "editor";
await loadProjectData();
} catch (error: any) {
errorMsg.value = `Error creando comando: ${error}`;
console.error("Error:", error);
}
}
// Guardar nuevo evento
async function saveEvent(_event: Event, code: string, savePath: string) {
try {
const fullPath = `${projectRoot.value}/${savePath}`;
await invoke("write_file_content", {
filePath: fullPath,
content: code
});
showNotification("✅ Evento creado correctamente");
currentView.value = "editor";
await loadProjectData();
} catch (error: any) {
errorMsg.value = `Error creando evento: ${error}`;
console.error("Error:", error);
}
}
// Cerrar creadores
function closeCreator() {
currentView.value = "editor";
}
// Mostrar notificación temporal
function showNotification(message: string, type: 'success' | 'error' | 'info' = 'info') {
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Manejar selección de proyecto
function handleProjectPathSelected(path: string) {
projectRoot.value = path;
showProjectSelector.value = false;
loadProjectData();
}
// Cambiar directorio del proyecto
function changeProjectDirectory() {
showProjectSelector.value = true;
}
// Toggle modo Dev Ultra
async function toggleDevUltra() {
devUltraMode.value = !devUltraMode.value;
if (devUltraMode.value) {
showNotification("⚡ Modo Dev Ultra Activado - Cargando archivos...");
// Cargar todos los archivos del proyecto
try {
allFiles.value = await invoke<FileInfo[]>("scan_all_files", {
projectRoot: projectRoot.value
});
} catch (error: any) {
errorMsg.value = `Error cargando archivos: ${error}`;
console.error("Error:", error);
}
} else {
showNotification("🔒 Modo Dev Ultra Desactivado");
allFiles.value = [];
}
}
// Toggle Database Viewer
function toggleDatabase() {
if (currentView.value === 'database') {
currentView.value = 'editor';
updateDiscordRPC("Navegando proyecto", "En el editor");
} else {
currentView.value = 'database';
updateDiscordRPC("Editando base de datos", "Visualizando schema.prisma");
}
}
// Toggle Env Manager
function toggleEnvManager() {
if (currentView.value === 'env-manager') {
currentView.value = 'editor';
updateDiscordRPC("Navegando proyecto", "En el editor");
} else {
currentView.value = 'env-manager';
updateDiscordRPC("Configurando variables", "Gestionando .env");
}
}
// Guardar schema de base de datos
async function saveSchema(content: string) {
try {
await invoke("write_file_content", {
filePath: `${projectRoot.value}/prisma/schema.prisma`,
content: content
});
schemaContent.value = content;
showNotification("✅ Schema guardado correctamente");
} catch (error: any) {
errorMsg.value = `Error guardando schema: ${error}`;
console.error("Error:", error);
}
}
// Manejar comandos del palette
function handlePaletteCommand(commandId: string) {
switch (commandId) {
case 'new-command':
showCommandCreator();
break;
case 'new-event':
showEventCreator();
break;
case 'refresh':
loadProjectData();
break;
case 'change-project':
changeProjectDirectory();
break;
case 'database':
toggleDatabase();
break;
case 'env-manager':
toggleEnvManager();
break;
case 'toggle-dev-ultra':
toggleDevUltra();
break;
case 'save':
if (selectedFile.value && fileContent.value) {
saveFile(fileContent.value);
}
break;
}
}
</script>
<template>
<div class="app-container">
<!-- Skeleton Loader -->
<SkeletonLoader v-if="initialLoading" />
<!-- Command Palette -->
<CommandPalette
:isOpen="showCommandPalette"
@close="showCommandPalette = false"
@command="handlePaletteCommand"
/>
<!-- Selector de proyecto -->
<ProjectSelector
v-if="showProjectSelector && !initialLoading"
@path-selected="handleProjectPathSelected"
/>
<template v-if="!showProjectSelector && !initialLoading">
<Sidebar
:stats="stats"
:commands="commands"
:events="events"
:allFiles="allFiles"
:selectedFile="selectedFile"
:projectRoot="projectRoot"
:devUltraMode="devUltraMode"
@new-command="showCommandCreator"
@new-event="showEventCreator"
@refresh="loadProjectData"
@select-file="selectFile"
@change-directory="changeProjectDirectory"
@toggle-dev-ultra="toggleDevUltra"
@toggle-database="toggleDatabase"
@toggle-env-manager="toggleEnvManager"
@notify="showNotification"
/>
<div class="main-content">
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"> Cargando proyecto...</div>
</div>
<div v-else-if="errorMsg" class="error-banner">
{{ errorMsg }}
<button @click="errorMsg = ''" class="close-error"></button>
</div>
<!-- Editor Monaco -->
<MonacoEditor
v-if="currentView === 'editor' && selectedFile"
:fileInfo="selectedFile"
:content="fileContent"
@save="saveFile"
/>
<!-- Command Creator -->
<CommandCreator
v-if="currentView === 'command-creator'"
@save="saveCommand"
@close="closeCreator"
/>
<!-- Event Creator -->
<EventCreator
v-if="currentView === 'event-creator'"
@save="saveEvent"
@close="closeCreator"
/>
<!-- Database Viewer -->
<DatabaseViewer
v-if="currentView === 'database'"
:schemaContent="schemaContent"
:projectRoot="projectRoot"
@save="saveSchema"
/>
<!-- Environment Manager -->
<EnvManager
v-if="currentView === 'env-manager'"
:projectRoot="projectRoot"
@close="() => currentView = 'editor'"
@notify="showNotification"
/>
<!-- Welcome Screen -->
<div v-if="currentView === 'editor' && !selectedFile" class="welcome-screen">
<div class="welcome-content">
<h1>🤖 Amayo Bot Editor</h1>
<p>Editor estilo VS Code para tu bot de Discord</p>
<div class="welcome-stats">
<div class="welcome-stat">
<div class="stat-number">{{ stats.totalCommands }}</div>
<div class="stat-label">Comandos Totales</div>
</div>
<div class="welcome-stat">
<div class="stat-number">{{ stats.totalEvents }}</div>
<div class="stat-label">Eventos Totales</div>
</div>
</div>
<div class="welcome-actions">
<button @click="showCommandCreator" class="welcome-btn primary">
Crear Comando
</button>
<button @click="showEventCreator" class="welcome-btn primary">
Crear Evento
</button>
</div>
<p class="welcome-hint">
💡 <strong>Tip:</strong> Selecciona un archivo del panel izquierdo para editarlo
</p>
</div>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.app-container {
display: flex;
height: 100vh;
overflow: hidden;
background-color: #1e1e1e;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(30, 30, 30, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
color: #ffffff;
font-size: 18px;
font-weight: 500;
}
.error-banner {
background-color: #f14c4c;
color: #ffffff;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 500;
}
.close-error {
background: none;
border: none;
color: #ffffff;
font-size: 18px;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
}
.close-error:hover {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.welcome-screen {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: #1e1e1e;
}
.welcome-content {
text-align: center;
max-width: 600px;
padding: 40px;
}
.welcome-content h1 {
color: #ffffff;
font-size: 48px;
margin: 0 0 16px 0;
font-weight: 700;
}
.welcome-content > p {
color: #cccccc;
font-size: 18px;
margin: 0 0 40px 0;
}
.welcome-stats {
display: flex;
gap: 40px;
justify-content: center;
margin-bottom: 40px;
}
.welcome-stat {
text-align: center;
}
.stat-number {
font-size: 48px;
font-weight: 700;
color: #4ec9b0;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #cccccc;
font-weight: 500;
}
.welcome-actions {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 40px;
}
.welcome-btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.2s;
}
.welcome-btn.primary {
background-color: #0e639c;
color: #ffffff;
}
.welcome-btn.primary:hover {
background-color: #1177bb;
transform: translateY(-2px);
}
.welcome-hint {
color: #858585;
font-size: 14px;
margin: 0;
padding: 16px;
background-color: #252526;
border-radius: 6px;
border-left: 3px solid #0e639c;
}
.welcome-hint strong {
color: #4ec9b0;
}
</style>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
overflow: hidden;
}
/* Notificaciones */
.notification {
position: fixed;
top: 20px;
right: 20px;
color: #ffffff;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
animation: slideIn 0.3s ease-out;
}
.notification-success {
background-color: #4ec9b0;
}
.notification-error {
background-color: #f48771;
}
.notification-info {
background-color: #007acc;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,713 @@
<template>
<div class="command-creator">
<div class="creator-header">
<h2>{{ isEditing ? 'Editar Comando' : 'Crear Nuevo Comando' }}</h2>
<button @click="$emit('close')" class="close-btn"></button>
</div>
<div class="creator-content">
<div class="form-section">
<div class="form-group">
<label>Tipo de Comando *</label>
<select v-model="commandData.type">
<option value="message">Comando por Mensaje (prefix)</option>
<option value="slash">Comando Slash (/comando)</option>
</select>
</div>
<div class="form-group">
<label>Nombre del Comando *</label>
<input
v-model="commandData.name"
type="text"
placeholder="ping, help, user-info..."
/>
</div>
<div class="form-group">
<label>Descripción *</label>
<input
v-model="commandData.description"
type="text"
placeholder="Describe qué hace este comando"
/>
</div>
<div class="form-row">
<div class="form-group">
<label>Categoría</label>
<input
v-model="commandData.category"
type="text"
placeholder="Utilidad, Diversión, Admin..."
/>
</div>
<div class="form-group">
<label>Cooldown (segundos)</label>
<input
v-model.number="commandData.cooldown"
type="number"
min="0"
placeholder="0"
/>
</div>
</div>
<div v-if="commandData.type === 'message'" class="form-group">
<label>Aliases (separados por coma)</label>
<input
v-model="aliasesInput"
type="text"
placeholder="p, pong, latencia"
/>
</div>
<div v-if="commandData.type === 'message'" class="form-group">
<label>Uso</label>
<input
v-model="commandData.usage"
type="text"
placeholder="!comando [arg1] [arg2]"
/>
</div>
<div class="form-group">
<label>Ruta de Guardado *</label>
<input
v-model="commandData.savePath"
type="text"
:placeholder="getDefaultPath()"
/>
<small>La ruta relativa donde se guardará el archivo .ts</small>
</div>
</div>
<div class="editor-section">
<div class="editor-header-small">
<h3>Función run() - Lógica del Comando</h3>
</div>
<div class="monaco-wrapper" ref="editorContainer"></div>
</div>
</div>
<div class="creator-footer">
<button @click="$emit('close')" class="cancel-btn">Cancelar</button>
<button @click="saveCommand" :disabled="!isValid" class="save-btn">
{{ isEditing ? '💾 Guardar Cambios' : ' Crear Comando' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import * as monaco from 'monaco-editor';
import type { Command } from '../types/bot';
const props = defineProps<{
initialCommand?: Command;
isEditing?: boolean;
}>();
const emit = defineEmits<{
'save': [command: Command, code: string, savePath: string];
'close': [];
}>();
const commandData = ref({
name: '',
type: 'message' as 'message' | 'slash',
description: '',
category: '',
cooldown: 0,
usage: '',
savePath: '',
});
const aliasesInput = ref('');
const editorContainer = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor | null = null;
const isValid = computed(() => {
return commandData.value.name.trim() !== '' &&
commandData.value.description.trim() !== '' &&
commandData.value.savePath.trim() !== '';
});
function getDefaultPath(): string {
const type = commandData.value.type === 'slash' ? 'splashcmd' : 'messages';
const category = commandData.value.category?.toLowerCase() || 'others';
return `src/commands/${type}/${category}/${commandData.value.name}.ts`;
}
watch(() => commandData.value, () => {
if (!commandData.value.savePath || commandData.value.savePath === '') {
commandData.value.savePath = getDefaultPath();
}
// Actualizar el editor en tiempo real con los cambios del formulario
if (editor) {
const currentPosition = editor.getPosition();
const newCode = getDefaultCode();
editor.setValue(newCode);
if (currentPosition) {
editor.setPosition(currentPosition);
}
}
}, { deep: true });
// Watch para aliases que también actualiza el editor
watch(() => aliasesInput.value, () => {
if (editor) {
const currentPosition = editor.getPosition();
const newCode = getDefaultCode();
editor.setValue(newCode);
if (currentPosition) {
editor.setPosition(currentPosition);
}
}
});
function getDefaultCode(): string {
if (commandData.value.type === 'slash') {
return `import type { ChatInputCommandInteraction } from "discord.js";
import type Amayo from "../../core/client";
export default {
name: "${commandData.value.name}",
description: "${commandData.value.description}",
type: 'slash' as const,
${commandData.value.cooldown > 0 ? `cooldown: ${commandData.value.cooldown},\n ` : ''}async run(interaction: ChatInputCommandInteraction, client: Amayo) {
// Tu código aquí
await interaction.reply({
content: "¡Comando ${commandData.value.name} ejecutado!",
ephemeral: true
});
}
}`;
} else {
return `import type { Message } from "discord.js";
import type Amayo from "../../core/client";
export default {
name: "${commandData.value.name}",
description: "${commandData.value.description}",
type: 'message' as const,
${commandData.value.category ? `category: "${commandData.value.category}",\n ` : ''}${commandData.value.usage ? `usage: "${commandData.value.usage}",\n ` : ''}${aliasesInput.value ? `aliases: [${aliasesInput.value.split(',').map(a => `"${a.trim()}"`).join(', ')}],\n ` : ''}${commandData.value.cooldown > 0 ? `cooldown: ${commandData.value.cooldown},\n ` : ''}async run(message: Message, args: string[], client: Amayo) {
// Tu código aquí
await message.reply("¡Comando ${commandData.value.name} ejecutado!");
}
}`;
}
}
function saveCommand() {
if (!isValid.value || !editor) return;
const code = editor.getValue();
const aliases = aliasesInput.value
.split(',')
.map(a => a.trim())
.filter(a => a !== '');
const command: Command = {
name: commandData.value.name,
type: commandData.value.type,
description: commandData.value.description,
cooldown: commandData.value.cooldown > 0 ? commandData.value.cooldown : undefined,
run: code,
} as Command;
if (commandData.value.type === 'message') {
(command as any).category = commandData.value.category || undefined;
(command as any).usage = commandData.value.usage || undefined;
if (aliases.length > 0) {
(command as any).aliases = aliases;
}
}
emit('save', command, code, commandData.value.savePath);
}
onMounted(() => {
if (editorContainer.value) {
monaco.editor.defineTheme('vs-dark-custom', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#1e1e1e',
}
});
const initialCode = props.initialCommand?.run || getDefaultCode();
editor = monaco.editor.create(editorContainer.value, {
value: initialCode,
language: 'typescript',
theme: 'vs-dark-custom',
automaticLayout: true,
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
tabSize: 2,
suggestOnTriggerCharacters: true,
quickSuggestions: {
other: true,
comments: false,
strings: true
},
});
// Configurar autocompletado para Discord.js + TypeScript snippets nativos
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
allowJs: true,
strict: true,
noImplicitAny: true,
strictNullChecks: true,
strictFunctionTypes: true,
});
// Habilitar snippets de TypeScript nativos y Discord.js
monaco.languages.registerCompletionItemProvider('typescript', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
const suggestions: monaco.languages.CompletionItem[] = [
{
label: 'try-catch',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'try {',
'\t${1:// Tu código aquí}',
'} catch (error) {',
'\tconsole.error(error);',
'\t${2:// Manejo de error}',
'}'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Try-catch block',
range
},
{
label: 'async-function',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'async function ${1:name}(${2:params}) {',
'\t${3:// Código asíncrono}',
'}'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Async function declaration',
range
},
{
label: 'discord-embed',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'embeds: [{',
'\ttitle: "${1:Título}",',
'\tdescription: "${2:Descripción}",',
'\tcolor: 0x${3:0099ff},',
'\tfields: [',
'\t\t{ name: "${4:Campo}", value: "${5:Valor}", inline: ${6:true} }',
'\t],',
'\ttimestamp: new Date(),',
'\tfooter: { text: "${7:Footer}" }',
'}]'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Discord embed structure',
range
},
{
label: 'message-reply',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'await message.reply("${1:Mensaje}");',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Responder a un mensaje',
range
},
{
label: 'interaction-reply',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'await interaction.reply({ content: "${1:Mensaje}", ephemeral: ${2:true} });',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Responder a una interacción slash',
range
},
{
label: 'interaction-defer',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'await interaction.deferReply({ ephemeral: ${1:true} });',
'${2:// Tu código largo aquí}',
'await interaction.editReply({ content: "${3:¡Listo!}" });'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Diferir respuesta de interacción',
range
},
{
label: 'logger-info',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'logger.info("${1:Mensaje de log}");',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Log de información',
range
},
{
label: 'logger-error',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'logger.error({ err: ${1:error} }, "${2:Mensaje de error}");',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Log de error con contexto',
range
},
{
label: 'prisma-findUnique',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'const ${1:result} = await prisma.${2:model}.findUnique({',
'\twhere: { ${3:id}: ${4:value} }',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Buscar registro único en Prisma',
range
},
{
label: 'prisma-create',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'const ${1:result} = await prisma.${2:model}.create({',
'\tdata: {',
'\t\t${3:field}: ${4:value}',
'\t}',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Crear registro en Prisma',
range
},
{
label: 'prisma-update',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'const ${1:result} = await prisma.${2:model}.update({',
'\twhere: { ${3:id}: ${4:value} },',
'\tdata: {',
'\t\t${5:field}: ${6:newValue}',
'\t}',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Actualizar registro en Prisma',
range
},
{
label: 'check-permissions',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'if (!message.member?.permissions.has("${1:Administrator}")) {',
'\treturn message.reply("❌ No tienes permisos.");',
'}'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Verificar permisos del usuario',
range
},
{
label: 'check-args',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'if (args.length < ${1:1}) {',
'\treturn message.reply("❌ Uso: ${2:!comando <arg>}");',
'}'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Verificar argumentos del comando',
range
}
];
return { suggestions };
}
});
}
// Cargar datos si está editando
if (props.initialCommand) {
commandData.value = {
name: props.initialCommand.name,
type: props.initialCommand.type,
description: props.initialCommand.description || '',
category: (props.initialCommand as any).category || '',
cooldown: props.initialCommand.cooldown || 0,
usage: (props.initialCommand as any).usage || '',
savePath: '',
};
if (props.initialCommand.type === 'message') {
aliasesInput.value = ((props.initialCommand as any).aliases || []).join(', ');
}
}
});
onUnmounted(() => {
if (editor) {
editor.dispose();
}
});
</script>
<style scoped>
.command-creator {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #1e1e1e;
color: #cccccc;
}
.creator-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background-color: #2d2d30;
border-bottom: 1px solid #3e3e42;
}
.creator-header h2 {
margin: 0;
font-size: 18px;
color: #ffffff;
}
.close-btn {
background: none;
border: none;
color: #cccccc;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
}
.close-btn:hover {
color: #ffffff;
background-color: #3e3e42;
border-radius: 4px;
}
.creator-content {
flex: 1;
display: flex;
overflow: hidden;
}
.form-section {
width: 400px;
padding: 24px;
overflow-y: auto;
border-right: 1px solid #3e3e42;
}
.code-preview {
margin-bottom: 24px;
border: 1px solid #3e3e42;
border-radius: 4px;
overflow: hidden;
background-color: #1e1e1e;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background-color: #2d2d30;
border-bottom: 1px solid #3e3e42;
}
.preview-title {
font-size: 12px;
font-weight: 600;
color: #ffffff;
}
.preview-badge {
font-size: 10px;
padding: 3px 8px;
background-color: #4ec9b0;
color: #1e1e1e;
border-radius: 3px;
font-weight: 600;
}
.preview-code {
margin: 0;
padding: 12px;
background-color: #1e1e1e;
overflow-x: auto;
max-height: 200px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
color: #d4d4d4;
}
.preview-code code {
white-space: pre;
color: #d4d4d4;
}
.form-group {
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 16px;
}
.form-row .form-group {
flex: 1;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: #ffffff;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
background-color: #3c3c3c;
border: 1px solid #3e3e42;
color: #cccccc;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #0e639c;
background-color: #2d2d30;
}
.form-group small {
display: block;
margin-top: 4px;
font-size: 11px;
color: #858585;
}
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-header-small {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
background-color: #252526;
border-bottom: 1px solid #3e3e42;
}
.editor-header-small h3 {
margin: 0;
font-size: 14px;
color: #ffffff;
}
.monaco-wrapper {
flex: 1;
overflow: hidden;
}
.creator-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background-color: #2d2d30;
border-top: 1px solid #3e3e42;
}
.cancel-btn,
.save-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
}
.cancel-btn {
background-color: #3e3e42;
color: #cccccc;
}
.cancel-btn:hover {
background-color: #4e4e52;
}
.save-btn {
background-color: #0e639c;
color: #ffffff;
}
.save-btn:hover:not(:disabled) {
background-color: #1177bb;
}
.save-btn:disabled {
background-color: #3e3e42;
opacity: 0.5;
cursor: not-allowed;
}
/* Scrollbar */
.form-section::-webkit-scrollbar {
width: 10px;
}
.form-section::-webkit-scrollbar-track {
background: #1e1e1e;
}
.form-section::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 5px;
}
</style>

View File

@@ -0,0 +1,330 @@
<template>
<div v-if="isOpen" class="palette-overlay" @click="close">
<div class="palette-container" @click.stop>
<div class="palette-header">
<input
ref="searchInput"
v-model="searchQuery"
type="text"
placeholder="Buscar comandos (Ctrl+Q)..."
class="palette-search"
@keydown.down.prevent="navigateDown"
@keydown.up.prevent="navigateUp"
@keydown.enter.prevent="executeSelected"
@keydown.esc="close"
/>
</div>
<div class="palette-results">
<div
v-for="(cmd, index) in filteredCommands"
:key="cmd.id"
:class="['palette-item', { active: index === selectedIndex }]"
@click="execute(cmd)"
@mouseenter="selectedIndex = index"
>
<span class="palette-icon">{{ cmd.icon }}</span>
<div class="palette-info">
<div class="palette-name">{{ cmd.name }}</div>
<div class="palette-desc">{{ cmd.description }}</div>
</div>
<kbd v-if="cmd.shortcut" class="palette-shortcut">{{ cmd.shortcut }}</kbd>
</div>
<div v-if="filteredCommands.length === 0" class="palette-empty">
No se encontraron comandos
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
interface PaletteCommand {
id: string;
name: string;
description: string;
icon: string;
shortcut?: string;
action: () => void;
}
const props = defineProps<{
isOpen: boolean;
}>();
const emit = defineEmits<{
'close': [];
'command': [commandId: string];
}>();
const searchQuery = ref('');
const selectedIndex = ref(0);
const searchInput = ref<HTMLInputElement | null>(null);
// Comandos disponibles (se pasarán desde App.vue)
const commands = ref<PaletteCommand[]>([
{
id: 'new-command',
name: 'Crear Nuevo Comando',
description: 'Crear un comando de mensaje o slash',
icon: '',
action: () => emit('command', 'new-command')
},
{
id: 'new-event',
name: 'Crear Nuevo Evento',
description: 'Crear un manejador de eventos',
icon: '⚡',
action: () => emit('command', 'new-event')
},
{
id: 'refresh',
name: 'Actualizar Proyecto',
description: 'Recargar estadísticas y archivos',
icon: '🔄',
shortcut: 'Ctrl+R',
action: () => emit('command', 'refresh')
},
{
id: 'change-project',
name: 'Cambiar Proyecto',
description: 'Seleccionar otro directorio',
icon: '📁',
action: () => emit('command', 'change-project')
},
{
id: 'database',
name: 'Ver Base de Datos',
description: 'Abrir visor de Prisma schema',
icon: '🗄️',
action: () => emit('command', 'database')
},
{
id: 'toggle-dev-ultra',
name: 'Modo Dev Ultra',
description: 'Habilitar edición completa del src/',
icon: '⚡',
action: () => emit('command', 'toggle-dev-ultra')
},
{
id: 'save',
name: 'Guardar Archivo',
description: 'Guardar el archivo actual',
icon: '💾',
shortcut: 'Ctrl+S',
action: () => emit('command', 'save')
},
]);
const filteredCommands = computed(() => {
if (!searchQuery.value) return commands.value;
const query = searchQuery.value.toLowerCase();
return commands.value.filter(cmd =>
cmd.name.toLowerCase().includes(query) ||
cmd.description.toLowerCase().includes(query)
);
});
function navigateDown() {
if (selectedIndex.value < filteredCommands.value.length - 1) {
selectedIndex.value++;
}
}
function navigateUp() {
if (selectedIndex.value > 0) {
selectedIndex.value--;
}
}
function executeSelected() {
const cmd = filteredCommands.value[selectedIndex.value];
if (cmd) {
execute(cmd);
}
}
function execute(cmd: PaletteCommand) {
cmd.action();
close();
}
function close() {
emit('close');
searchQuery.value = '';
selectedIndex.value = 0;
}
// Focus en el input cuando se abre
watch(() => props.isOpen, async (isOpen) => {
if (isOpen) {
await nextTick();
searchInput.value?.focus();
}
});
onMounted(() => {
// Atajos de teclado globales
const handleKeydown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'q') {
e.preventDefault();
if (props.isOpen) {
close();
}
}
};
window.addEventListener('keydown', handleKeydown);
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
});
});
</script>
<style scoped>
.palette-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 15vh;
z-index: 10000;
backdrop-filter: blur(2px);
animation: fadeIn 0.15s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.palette-container {
width: 600px;
max-height: 500px;
background-color: #252526;
border: 1px solid #3e3e42;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
overflow: hidden;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.palette-header {
padding: 12px;
border-bottom: 1px solid #3e3e42;
}
.palette-search {
width: 100%;
padding: 10px 14px;
background-color: #3c3c3c;
border: 1px solid #3e3e42;
color: #cccccc;
font-size: 15px;
border-radius: 4px;
outline: none;
font-family: inherit;
}
.palette-search:focus {
border-color: #0e639c;
background-color: #2d2d30;
}
.palette-results {
max-height: 400px;
overflow-y: auto;
}
.palette-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #2d2d30;
transition: background-color 0.1s;
}
.palette-item:hover,
.palette-item.active {
background-color: #2d2d30;
}
.palette-icon {
font-size: 20px;
width: 28px;
text-align: center;
}
.palette-info {
flex: 1;
}
.palette-name {
color: #ffffff;
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
}
.palette-desc {
color: #858585;
font-size: 12px;
}
.palette-shortcut {
background-color: #3c3c3c;
color: #cccccc;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
font-family: 'Consolas', 'Monaco', monospace;
border: 1px solid #4e4e52;
}
.palette-empty {
padding: 40px 20px;
text-align: center;
color: #858585;
font-size: 14px;
}
.palette-results::-webkit-scrollbar {
width: 10px;
}
.palette-results::-webkit-scrollbar-track {
background: #1e1e1e;
}
.palette-results::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 5px;
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<div class="db-viewer">
<div class="db-tabs">
<button
:class="['db-tab', { active: currentTab === 'schema' }]"
@click="currentTab = 'schema'"
>
📝 Schema
</button>
<button
:class="['db-tab', { active: currentTab === 'diagram' }]"
@click="currentTab = 'diagram'"
>
🗺 Diagrama
</button>
</div>
<!-- Vista del Schema (Editor Monaco) - Usar v-show para mantenerlo montado -->
<div v-show="currentTab === 'schema'" class="schema-editor">
<div ref="schemaEditorContainer" class="monaco-container"></div>
</div>
<!-- Vista del Diagrama (Visual) -->
<div v-show="currentTab === 'diagram'" class="diagram-view">
<div class="diagram-canvas" ref="diagramCanvas">
<div
v-for="table in tables"
:key="table.name"
class="table-card"
>
<div class="table-header">
<span class="table-icon">🗃</span>
<span class="table-name">{{ table.name }}</span>
</div>
<div class="table-fields">
<div v-for="field in table.fields" :key="field.name" class="table-field">
<span :class="['field-icon', { primary: field.isPrimary }]">
{{ field.isPrimary ? '🔑' : '●' }}
</span>
<span class="field-name">{{ field.name }}</span>
<span class="field-type">{{ field.type }}</span>
</div>
</div>
<div v-if="table.relations.length > 0" class="table-relations">
<div class="relations-title">🔗 Relaciones</div>
<div v-for="rel in table.relations" :key="rel" class="relation-item">
{{ rel }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as monaco from 'monaco-editor';
interface TableField {
name: string;
type: string;
isPrimary: boolean;
}
interface Table {
name: string;
fields: TableField[];
relations: string[];
x: number;
y: number;
}
const props = defineProps<{
schemaContent: string;
projectRoot: string;
}>();
const emit = defineEmits<{
'save': [content: string];
}>();
const currentTab = ref<'schema' | 'diagram'>('schema');
const schemaEditorContainer = ref<HTMLElement | null>(null);
let schemaEditor: monaco.editor.IStandaloneCodeEditor | null = null;
const tables = ref<Table[]>([]);
// Watch para actualizar el editor cuando cambie schemaContent
watch(() => props.schemaContent, (newContent) => {
if (schemaEditor && schemaEditor.getValue() !== newContent) {
schemaEditor.setValue(newContent);
}
parseSchema();
});
// Parsear el schema de Prisma para extraer tablas
function parseSchema() {
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
const fieldRegex = /(\w+)\s+([\w\[\]?]+)(?:.*@id)?/g;
const parsedTables: Table[] = [];
let match;
while ((match = modelRegex.exec(props.schemaContent)) !== null) {
const tableName = match[1];
const tableBody = match[2];
const fields: TableField[] = [];
const relations: string[] = [];
const fieldMatches = tableBody.matchAll(fieldRegex);
for (const fieldMatch of fieldMatches) {
const fieldName = fieldMatch[1];
const fieldType = fieldMatch[2];
const isPrimary = tableBody.includes(`${fieldName}.*@id`);
if (!['model', 'enum'].includes(fieldName)) {
fields.push({
name: fieldName,
type: fieldType,
isPrimary
});
}
}
// Buscar relaciones
const relationMatches = tableBody.matchAll(/(\w+)\s+(\w+)(?:\[\])?/g);
for (const relMatch of relationMatches) {
const relType = relMatch[2];
if (relType[0] === relType[0].toUpperCase() && relType !== 'String' && relType !== 'Int' && relType !== 'Boolean' && relType !== 'DateTime' && relType !== 'Json') {
if (!relations.includes(relType)) {
relations.push(relType);
}
}
}
parsedTables.push({
name: tableName,
fields: fields,
relations,
x: 0,
y: 0
});
}
tables.value = parsedTables;
}
onMounted(() => {
// Crear editor Monaco para el schema
if (schemaEditorContainer.value) {
schemaEditor = monaco.editor.create(schemaEditorContainer.value, {
value: props.schemaContent,
language: 'prisma',
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: true },
fontSize: 13,
lineNumbers: 'on',
readOnly: false,
});
// Guardar con Ctrl+S
schemaEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
if (schemaEditor) {
emit('save', schemaEditor.getValue());
}
});
}
// Parsear el schema para el diagrama
parseSchema();
});
onUnmounted(() => {
if (schemaEditor) {
schemaEditor.dispose();
}
});
</script>
<style scoped>
.db-viewer {
display: flex;
flex-direction: column;
height: 100%;
background-color: #1e1e1e;
}
.db-tabs {
display: flex;
background-color: #2d2d30;
border-bottom: 1px solid #3e3e42;
}
.db-tab {
padding: 12px 24px;
background: none;
border: none;
color: #cccccc;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.db-tab:hover {
background-color: #3e3e42;
color: #ffffff;
}
.db-tab.active {
color: #ffffff;
border-bottom-color: #0e639c;
background-color: #1e1e1e;
}
.schema-editor {
flex: 1;
overflow: hidden;
}
.monaco-container {
width: 100%;
height: 100%;
}
.diagram-view {
flex: 1;
overflow: auto;
background-color: #1e1e1e;
background-image:
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 20px 20px;
}
.diagram-canvas {
position: relative;
min-width: max-content;
min-height: max-content;
padding: 30px;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-content: flex-start;
}
.table-card {
width: 240px;
background-color: #1f1f1f;
border: 1px solid #3e3e42;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
transition: all 0.2s;
position: relative;
}
.table-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
border-color: #007acc;
}
.table-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: linear-gradient(135deg, #2d2d30 0%, #252526 100%);
border-bottom: 2px solid #007acc;
}
.table-icon {
font-size: 16px;
}
.table-name {
color: #ffffff;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.5px;
}
.table-fields {
padding: 8px 0;
max-height: none;
overflow-y: visible;
}
.table-field {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
transition: background-color 0.15s;
}
.table-field:hover {
background-color: #2d2d30;
}
.field-icon {
font-size: 8px;
opacity: 0.7;
color: #858585;
}
.field-icon.primary {
opacity: 1;
font-size: 12px;
}
.field-name {
flex: 1;
color: #d4d4d4;
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.field-type {
color: #4ec9b0;
font-size: 11px;
font-family: 'Consolas', 'Monaco', monospace;
font-weight: 500;
}
.table-relations {
padding: 10px 12px;
background-color: #252526;
border-top: 1px solid #3e3e42;
max-height: none;
overflow-y: visible;
}
.relations-title {
color: #d7ba7d;
font-size: 11px;
font-weight: 700;
margin-bottom: 6px;
}
.relation-item {
color: #d4d4d4;
font-size: 11px;
margin-bottom: 4px;
padding: 4px 8px;
background-color: #1e1e1e;
border-left: 3px solid #007acc;
border-radius: 2px;
font-family: 'Consolas', 'Monaco', monospace;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,635 @@
<template>
<div class="event-creator">
<div class="creator-header">
<h2>{{ isEditing ? 'Editar Evento' : 'Crear Nuevo Evento' }}</h2>
<button @click="$emit('close')" class="close-btn"></button>
</div>
<div class="creator-content">
<div class="form-section">
<div class="form-group">
<label>Tipo de Evento *</label>
<select v-model="eventData.eventType">
<option value="standard">Evento Estándar de Discord.js</option>
<option value="extra">Evento Custom/Extra</option>
</select>
</div>
<div v-if="eventData.eventType === 'standard'" class="form-group">
<label>Evento de Discord *</label>
<select v-model="eventData.discordEvent">
<option value="">Selecciona un evento...</option>
<option value="ready">ready - Bot está listo</option>
<option value="messageCreate">messageCreate - Nuevo mensaje</option>
<option value="interactionCreate">interactionCreate - Interacción recibida</option>
<option value="guildCreate">guildCreate - Bot añadido a servidor</option>
<option value="guildDelete">guildDelete - Bot removido de servidor</option>
<option value="guildMemberAdd">guildMemberAdd - Miembro se une</option>
<option value="guildMemberRemove">guildMemberRemove - Miembro sale</option>
<option value="messageDelete">messageDelete - Mensaje eliminado</option>
<option value="messageUpdate">messageUpdate - Mensaje editado</option>
<option value="channelCreate">channelCreate - Canal creado</option>
<option value="channelDelete">channelDelete - Canal eliminado</option>
</select>
</div>
<div class="form-group">
<label>Nombre del Archivo *</label>
<input
v-model="eventData.fileName"
type="text"
placeholder="myCustomEvent, allianceHandler..."
/>
<small>Nombre del archivo .ts (sin extensión)</small>
</div>
<div class="form-group">
<label>Descripción</label>
<textarea
v-model="eventData.description"
rows="3"
placeholder="Describe qué hace este evento..."
></textarea>
</div>
<div class="form-group">
<label>Ruta de Guardado *</label>
<input
v-model="eventData.savePath"
type="text"
:placeholder="getDefaultPath()"
/>
<small>La ruta relativa donde se guardará el archivo .ts</small>
</div>
<div v-if="eventData.eventType === 'extra'" class="info-box">
<p><strong> Eventos Custom/Extra:</strong></p>
<p>Los eventos extras son funciones que se ejecutan dentro de eventos estándar (como messageCreate).
Deben exportar una función que será llamada desde el evento principal.</p>
</div>
</div>
<div class="editor-section">
<div class="editor-header-small">
<h3>Código del Evento</h3>
</div>
<div class="monaco-wrapper" ref="editorContainer"></div>
</div>
</div>
<div class="creator-footer">
<button @click="$emit('close')" class="cancel-btn">Cancelar</button>
<button @click="saveEvent" :disabled="!isValid" class="save-btn">
{{ isEditing ? '💾 Guardar Cambios' : ' Crear Evento' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import * as monaco from 'monaco-editor';
import type { Event } from '../types/bot';
const props = defineProps<{
initialEvent?: Event;
isEditing?: boolean;
}>();
const emit = defineEmits<{
'save': [event: Event, code: string, savePath: string];
'close': [];
}>();
const eventData = ref({
fileName: '',
eventType: 'standard' as 'standard' | 'extra',
discordEvent: '',
description: '',
savePath: '',
});
const editorContainer = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor | null = null;
const isValid = computed(() => {
if (eventData.value.eventType === 'standard') {
return eventData.value.fileName.trim() !== '' &&
eventData.value.discordEvent.trim() !== '' &&
eventData.value.savePath.trim() !== '';
}
return eventData.value.fileName.trim() !== '' &&
eventData.value.savePath.trim() !== '';
});
function getDefaultPath(): string {
if (eventData.value.eventType === 'extra') {
return `src/events/extras/${eventData.value.fileName}.ts`;
}
return `src/events/${eventData.value.fileName || eventData.value.discordEvent}.ts`;
}
watch(() => eventData.value, () => {
if (!eventData.value.savePath || eventData.value.savePath === '') {
eventData.value.savePath = getDefaultPath();
}
// Actualizar el editor en tiempo real con los cambios del formulario
if (editor) {
const currentPosition = editor.getPosition();
const newCode = getDefaultCode();
editor.setValue(newCode);
if (currentPosition) {
editor.setPosition(currentPosition);
}
}
}, { deep: true });
function getDefaultCode(): string {
if (eventData.value.eventType === 'extra') {
return `import { Message } from "discord.js";
import logger from "../../core/lib/logger";
/**
* ${eventData.value.description || 'Función custom que se ejecuta desde messageCreate'}
*/
export async function ${eventData.value.fileName || 'customHandler'}(message: Message) {
try {
// Verificar condiciones
if (message.author.bot) return;
// Tu lógica aquí
logger.info(\`Evento custom ejecutado para mensaje de \${message.author.tag}\`);
} catch (error) {
logger.error({ err: error }, "Error en evento custom");
}
}
`;
} else {
const eventName = eventData.value.discordEvent || 'ready';
const templates: Record<string, string> = {
ready: `import { bot } from "../main";
import { Events } from "discord.js";
import logger from "../core/lib/logger";
bot.on(Events.ClientReady, async (client) => {
logger.info(\`✅ Bot iniciado como \${client.user.tag}\`);
// Tu código aquí
});
`,
messageCreate: `import { bot } from "../main";
import { Events } from "discord.js";
import logger from "../core/lib/logger";
bot.on(Events.MessageCreate, async (message) => {
if (message.author.bot) return;
// Tu código aquí
logger.info(\`Mensaje recibido de \${message.author.tag}\`);
});
`,
interactionCreate: `import { bot } from "../main";
import { Events } from "discord.js";
import logger from "../core/lib/logger";
bot.on(Events.InteractionCreate, async (interaction) => {
// Tu código aquí
if (interaction.isChatInputCommand()) {
logger.info(\`Comando slash: \${interaction.commandName}\`);
}
});
`,
};
return templates[eventName] || `import { bot } from "../main";
import { Events } from "discord.js";
import logger from "../core/lib/logger";
bot.on(Events.${eventName.charAt(0).toUpperCase() + eventName.slice(1)}, async (...args) => {
// Tu código aquí
logger.info("Evento ${eventName} ejecutado");
});
`;
}
}
function saveEvent() {
if (!isValid.value || !editor) return;
const code = editor.getValue();
const event: Event = {
name: eventData.value.fileName,
type: eventData.value.eventType,
eventName: eventData.value.eventType === 'standard' ? eventData.value.discordEvent : undefined,
path: eventData.value.savePath,
code: code,
};
emit('save', event, code, eventData.value.savePath);
}
onMounted(() => {
if (editorContainer.value) {
monaco.editor.defineTheme('vs-dark-custom', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#1e1e1e',
}
});
const initialCode = props.initialEvent?.code || getDefaultCode();
editor = monaco.editor.create(editorContainer.value, {
value: initialCode,
language: 'typescript',
theme: 'vs-dark-custom',
automaticLayout: true,
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
tabSize: 2,
suggestOnTriggerCharacters: true,
quickSuggestions: {
other: true,
comments: false,
strings: true
},
});
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
strict: true,
noImplicitAny: true,
strictNullChecks: true,
strictFunctionTypes: true,
});
// Registrar snippets personalizados para eventos
monaco.languages.registerCompletionItemProvider('typescript', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
const suggestions: monaco.languages.CompletionItem[] = [
{
label: 'try-catch',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'try {',
'\t${1:// Tu código aquí}',
'} catch (error) {',
'\tlogger.error({ err: error }, "${2:Error en evento}");',
'}'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Try-catch block con logger',
range
},
{
label: 'logger-info',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'logger.info("${1:Mensaje de log}");',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Log de información',
range
},
{
label: 'logger-error',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'logger.error({ err: ${1:error} }, "${2:Mensaje de error}");',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Log de error con contexto',
range
},
{
label: 'check-bot-message',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'if (message.author.bot) return;',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Ignorar mensajes de bots',
range
},
{
label: 'check-guild',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'if (!message.guild) return;',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Verificar si está en un servidor',
range
},
{
label: 'check-content',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: 'if (!message.content || message.content.trim() === "") return;',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Verificar contenido del mensaje',
range
},
{
label: 'prisma-findUnique',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'const ${1:result} = await prisma.${2:model}.findUnique({',
'\twhere: { ${3:id}: ${4:value} }',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Buscar registro único en Prisma',
range
},
{
label: 'prisma-create',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'const ${1:result} = await prisma.${2:model}.create({',
'\tdata: {',
'\t\t${3:field}: ${4:value}',
'\t}',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Crear registro en Prisma',
range
},
{
label: 'discord-embed',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'const embed = {',
'\ttitle: "${1:Título}",',
'\tdescription: "${2:Descripción}",',
'\tcolor: 0x${3:0099ff},',
'\tfields: [',
'\t\t{ name: "${4:Campo}", value: "${5:Valor}", inline: ${6:true} }',
'\t],',
'\ttimestamp: new Date(),',
'\tfooter: { text: "${7:Footer}" }',
'};'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Embed de Discord',
range
},
{
label: 'event-ready',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'bot.on(Events.ClientReady, async (client) => {',
'\tlogger.info(`✅ Bot iniciado como \\${client.user.tag}`);',
'\t${1:// Tu código aquí}',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Evento ready de Discord',
range
},
{
label: 'event-messageCreate',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'bot.on(Events.MessageCreate, async (message) => {',
'\tif (message.author.bot) return;',
'\t${1:// Tu código aquí}',
'});'
].join('\n'),
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Evento messageCreate de Discord',
range
}
];
return { suggestions };
}
});
}
if (props.initialEvent) {
eventData.value = {
fileName: props.initialEvent.name,
eventType: props.initialEvent.type,
discordEvent: props.initialEvent.eventName || '',
description: '',
savePath: props.initialEvent.path,
};
}
});
onUnmounted(() => {
if (editor) {
editor.dispose();
}
});
</script>
<style scoped>
.event-creator {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #1e1e1e;
color: #cccccc;
}
.creator-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background-color: #2d2d30;
border-bottom: 1px solid #3e3e42;
}
.creator-header h2 {
margin: 0;
font-size: 18px;
color: #ffffff;
}
.close-btn {
background: none;
border: none;
color: #cccccc;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
}
.close-btn:hover {
color: #ffffff;
background-color: #3e3e42;
border-radius: 4px;
}
.creator-content {
flex: 1;
display: flex;
overflow: hidden;
}
.form-section {
width: 400px;
padding: 24px;
overflow-y: auto;
border-right: 1px solid #3e3e42;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: #ffffff;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
background-color: #3c3c3c;
border: 1px solid #3e3e42;
color: #cccccc;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
resize: vertical;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #0e639c;
background-color: #2d2d30;
}
.form-group small {
display: block;
margin-top: 4px;
font-size: 11px;
color: #858585;
}
.info-box {
padding: 12px;
background-color: #2d2d30;
border-left: 3px solid #0e639c;
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
}
.info-box p {
margin: 0 0 8px 0;
}
.info-box p:last-child {
margin-bottom: 0;
}
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-header-small {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
background-color: #252526;
border-bottom: 1px solid #3e3e42;
}
.editor-header-small h3 {
margin: 0;
font-size: 14px;
color: #ffffff;
}
.monaco-wrapper {
flex: 1;
overflow: hidden;
}
.creator-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background-color: #2d2d30;
border-top: 1px solid #3e3e42;
}
.cancel-btn,
.save-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
}
.cancel-btn {
background-color: #3e3e42;
color: #cccccc;
}
.cancel-btn:hover {
background-color: #4e4e52;
}
.save-btn {
background-color: #0e639c;
color: #ffffff;
}
.save-btn:hover:not(:disabled) {
background-color: #1177bb;
}
.save-btn:disabled {
background-color: #3e3e42;
opacity: 0.5;
cursor: not-allowed;
}
.form-section::-webkit-scrollbar {
width: 10px;
}
.form-section::-webkit-scrollbar-track {
background: #1e1e1e;
}
.form-section::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 5px;
}
</style>

View File

@@ -0,0 +1,841 @@
<template>
<div class="file-explorer">
<!-- Context Menu -->
<Teleport to="body">
<div
v-if="contextMenu.visible"
class="context-menu"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
@click.stop
>
<div class="context-menu-item" @click="showNewFileModal">
<span class="menu-icon">📄</span>
<span>Nuevo Archivo</span>
<span class="menu-shortcut">Ctrl+N</span>
</div>
<div class="context-menu-item" @click="showNewFolderModal">
<span class="menu-icon">📁</span>
<span>Nueva Carpeta</span>
<span class="menu-shortcut">Ctrl+Shift+N</span>
</div>
<div v-if="contextMenu.target" class="context-menu-divider"></div>
<div
v-if="contextMenu.target"
class="context-menu-item"
@click="showRenameModal"
>
<span class="menu-icon"></span>
<span>Renombrar</span>
<span class="menu-shortcut">F2</span>
</div>
<div
v-if="contextMenu.target"
class="context-menu-item danger"
@click="showDeleteConfirmation"
>
<span class="menu-icon">🗑</span>
<span>Eliminar</span>
<span class="menu-shortcut">Del</span>
</div>
</div>
</Teleport>
<!-- Modal: Nuevo Archivo/Carpeta/Renombrar -->
<Teleport to="body">
<div v-if="inputModal.visible" class="modal-overlay" @click="closeInputModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ inputModal.title }}</h3>
<button class="modal-close" @click="closeInputModal"></button>
</div>
<div class="modal-body">
<label class="input-label">{{ inputModal.label }}</label>
<input
ref="inputField"
v-model="inputModal.value"
type="text"
class="input-field"
:placeholder="inputModal.placeholder"
@keydown.enter="handleInputSubmit"
@keydown.esc="closeInputModal"
/>
<div v-if="inputModal.error" class="input-error">
{{ inputModal.error }}
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeInputModal">
Cancelar
</button>
<button
class="btn btn-primary"
@click="handleInputSubmit"
:disabled="!inputModal.value.trim()"
>
{{ inputModal.confirmText }}
</button>
</div>
</div>
</div>
</Teleport>
<!-- Modal: Confirmación de Eliminación -->
<Teleport to="body">
<div v-if="deleteModal.visible" class="modal-overlay" @click="closeDeleteModal">
<div class="modal-content modal-danger" @click.stop>
<div class="modal-header">
<h3> Confirmar Eliminación</h3>
<button class="modal-close" @click="closeDeleteModal"></button>
</div>
<div class="modal-body">
<p>¿Estás seguro de que deseas eliminar?</p>
<div class="delete-target">
<span class="file-icon">{{ deleteModal.isFolder ? '📁' : '📄' }}</span>
<strong>{{ deleteModal.targetName }}</strong>
</div>
<p class="warning-text">
Esta acción no se puede deshacer.
</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeDeleteModal">
Cancelar
</button>
<button
class="btn btn-danger"
@click="handleDelete"
>
Eliminar
</button>
</div>
</div>
</div>
</Teleport>
<!-- File Tree -->
<div class="file-tree" @click="closeContextMenu">
<template v-for="(files, folder) in groupedFiles" :key="folder">
<!-- Folder Header -->
<div
v-if="folder !== 'root'"
class="folder-item"
@contextmenu.prevent="openContextMenu($event, null, folder)"
>
<div class="folder-header">
<span class="folder-icon">📁</span>
<span class="folder-name">{{ folder }}</span>
<span class="file-count">{{ files.length }}</span>
</div>
</div>
<!-- Files in Folder -->
<div
v-for="file in files"
:key="file.path"
:class="['file-item', {
active: selectedFile?.path === file.path,
'in-folder': folder !== 'root'
}]"
@click="$emit('select-file', file)"
@contextmenu.prevent="openContextMenu($event, file, folder)"
>
<span class="file-icon">{{ getFileIcon(file) }}</span>
<span class="file-name">{{ file.name }}</span>
</div>
</template>
<div v-if="Object.keys(groupedFiles).length === 0" class="empty-state">
<p>Sin archivos</p>
<p class="empty-hint">Click derecho para crear archivos</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import type { FileInfo } from '../types/bot';
// Props
const props = defineProps<{
files: FileInfo[];
selectedFile: FileInfo | null;
projectRoot: string;
basePath: string; // 'commands' | 'events' | '' (para root)
}>();
// Emits
const emit = defineEmits<{
'select-file': [file: FileInfo];
'refresh': [];
'notify': [message: string, type: 'success' | 'error' | 'info'];
}>();
// State: Context Menu
const contextMenu = ref({
visible: false,
x: 0,
y: 0,
target: null as FileInfo | null,
folder: '' as string
});
// State: Input Modal (Nuevo/Renombrar)
const inputModal = ref({
visible: false,
type: '' as 'file' | 'folder' | 'rename',
title: '',
label: '',
placeholder: '',
confirmText: '',
value: '',
error: '',
targetFile: null as FileInfo | null,
targetFolder: ''
});
// State: Delete Modal
const deleteModal = ref({
visible: false,
target: null as FileInfo | null,
targetName: '',
isFolder: false
});
const inputField = ref<HTMLInputElement | null>(null);
// Computed: Agrupar archivos por carpeta
const groupedFiles = computed(() => {
const grouped: Record<string, FileInfo[]> = {};
console.log(`🗂️ FileExplorer recibió ${props.files.length} archivos para basePath="${props.basePath}"`);
props.files.forEach(file => {
const folder = file.folder || 'root';
if (!grouped[folder]) {
grouped[folder] = [];
}
grouped[folder].push(file);
});
console.log("📁 Grupos creados:", Object.keys(grouped), grouped);
return grouped;
});
// Context Menu
function openContextMenu(event: MouseEvent, file: FileInfo | null, folder: string) {
contextMenu.value = {
visible: true,
x: event.clientX,
y: event.clientY,
target: file,
folder: folder
};
}
function closeContextMenu() {
contextMenu.value.visible = false;
}
// Cerrar context menu al hacer click fuera
watch(() => contextMenu.value.visible, (visible) => {
if (visible) {
const handleClickOutside = () => {
closeContextMenu();
document.removeEventListener('click', handleClickOutside);
};
setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 0);
}
});
// Modales: Nuevo Archivo
function showNewFileModal() {
inputModal.value = {
visible: true,
type: 'file',
title: '📄 Nuevo Archivo',
label: 'Nombre del archivo:',
placeholder: 'ejemplo.ts',
confirmText: 'Crear',
value: '',
error: '',
targetFile: null,
targetFolder: contextMenu.value.folder
};
closeContextMenu();
nextTick(() => inputField.value?.focus());
}
// Modales: Nueva Carpeta
function showNewFolderModal() {
inputModal.value = {
visible: true,
type: 'folder',
title: '📁 Nueva Carpeta',
label: 'Nombre de la carpeta:',
placeholder: 'mi-carpeta',
confirmText: 'Crear',
value: '',
error: '',
targetFile: null,
targetFolder: contextMenu.value.folder
};
closeContextMenu();
nextTick(() => inputField.value?.focus());
}
// Modales: Renombrar
function showRenameModal() {
const target = contextMenu.value.target;
if (!target) return;
inputModal.value = {
visible: true,
type: 'rename',
title: '✏️ Renombrar',
label: 'Nuevo nombre:',
placeholder: target.name,
confirmText: 'Renombrar',
value: target.name,
error: '',
targetFile: target,
targetFolder: contextMenu.value.folder
};
closeContextMenu();
nextTick(() => {
if (inputField.value) {
inputField.value.focus();
// Seleccionar el nombre sin la extensión
const dotIndex = inputModal.value.value.lastIndexOf('.');
if (dotIndex > 0) {
inputField.value.setSelectionRange(0, dotIndex);
} else {
inputField.value.select();
}
}
});
}
function closeInputModal() {
inputModal.value.visible = false;
inputModal.value.value = '';
inputModal.value.error = '';
}
// Modales: Eliminar
function showDeleteConfirmation() {
const target = contextMenu.value.target;
if (!target) return;
deleteModal.value = {
visible: true,
target: target,
targetName: target.name,
isFolder: false // TODO: Detectar si es carpeta
};
closeContextMenu();
}
function closeDeleteModal() {
deleteModal.value.visible = false;
}
// Handlers: Crear/Renombrar
async function handleInputSubmit() {
const { type, value, targetFile, targetFolder } = inputModal.value;
if (!value.trim()) {
inputModal.value.error = 'El nombre no puede estar vacío';
return;
}
// Validar caracteres inválidos
const invalidChars = /[<>:"/\\|?*]/;
if (invalidChars.test(value)) {
inputModal.value.error = 'Nombre contiene caracteres inválidos';
return;
}
try {
if (type === 'file') {
await handleCreateFile(value, targetFolder);
} else if (type === 'folder') {
await handleCreateFolder(value, targetFolder);
} else if (type === 'rename' && targetFile) {
await handleRename(targetFile, value);
}
closeInputModal();
} catch (error: any) {
inputModal.value.error = error.toString();
}
}
// Backend Calls
async function handleCreateFile(filename: string, folder: string) {
try {
// Construir la ruta completa
let fullPath = props.projectRoot;
if (props.basePath) {
fullPath += '/' + props.basePath;
}
if (folder !== 'root') {
fullPath += '/' + folder;
}
fullPath += '/' + filename;
await invoke('create_file', { filePath: fullPath });
emit('notify', `✅ Archivo "${filename}" creado correctamente`, 'success');
emit('refresh');
} catch (error: any) {
emit('notify', `❌ Error creando archivo: ${error}`, 'error');
throw error;
}
}
async function handleCreateFolder(folderName: string, parentFolder: string) {
try {
// Construir la ruta completa
let fullPath = props.projectRoot;
if (props.basePath) {
fullPath += '/' + props.basePath;
}
if (parentFolder !== 'root') {
fullPath += '/' + parentFolder;
}
fullPath += '/' + folderName;
await invoke('create_folder', { folderPath: fullPath });
emit('notify', `✅ Carpeta "${folderName}" creada correctamente`, 'success');
emit('refresh');
} catch (error: any) {
emit('notify', `❌ Error creando carpeta: ${error}`, 'error');
throw error;
}
}
async function handleRename(file: FileInfo, newName: string) {
try {
const oldPath = `${props.projectRoot}/${file.relative_path}`;
const dirPath = file.relative_path.substring(0, file.relative_path.lastIndexOf('/'));
const newPath = `${props.projectRoot}/${dirPath}/${newName}`;
await invoke('rename_file', { oldPath, newPath });
emit('notify', `✅ Renombrado a "${newName}" correctamente`, 'success');
emit('refresh');
} catch (error: any) {
emit('notify', `❌ Error renombrando: ${error}`, 'error');
throw error;
}
}
async function handleDelete() {
const target = deleteModal.value.target;
if (!target) return;
try {
const fullPath = `${props.projectRoot}/${target.relative_path}`;
if (deleteModal.value.isFolder) {
await invoke('delete_folder', { folderPath: fullPath });
} else {
await invoke('delete_file', { filePath: fullPath });
}
emit('notify', `✅ Eliminado correctamente`, 'success');
emit('refresh');
closeDeleteModal();
} catch (error: any) {
emit('notify', `❌ Error eliminando: ${error}`, 'error');
}
}
// Utilidades
function getFileIcon(file: FileInfo): string {
if (file.commandType === 'message') return '💬';
if (file.commandType === 'slash') return '⚡';
if (file.eventType === 'standard') return '📡';
if (file.eventType === 'extra') return '🎯';
// Por extensión
const ext = file.name.split('.').pop()?.toLowerCase();
switch (ext) {
case 'ts': return '🔷';
case 'js': return '🟨';
case 'json': return '📋';
case 'md': return '📝';
default: return '📄';
}
}
</script>
<style scoped>
.file-explorer {
position: relative;
width: 100%;
}
/* Context Menu */
.context-menu {
position: fixed;
background: #2d2d30;
border: 1px solid #3e3e42;
border-radius: 6px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
padding: 4px;
min-width: 220px;
z-index: 10000;
animation: contextMenuSlideIn 0.15s ease-out;
}
@keyframes contextMenuSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.context-menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
font-size: 13px;
color: #cccccc;
transition: all 0.15s;
}
.context-menu-item:hover {
background: #37373d;
color: #ffffff;
}
.context-menu-item.danger:hover {
background: #5a1d1d;
color: #f48771;
}
.menu-icon {
font-size: 14px;
}
.menu-shortcut {
margin-left: auto;
font-size: 11px;
color: #858585;
}
.context-menu-divider {
height: 1px;
background: #3e3e42;
margin: 4px 0;
}
/* Modal Overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
background: #2d2d30;
border: 1px solid #3e3e42;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
width: 450px;
max-width: 90vw;
animation: modalSlideUp 0.25s ease-out;
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-danger {
border-color: #be1100;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #3e3e42;
}
.modal-header h3 {
margin: 0;
font-size: 16px;
color: #ffffff;
}
.modal-close {
background: none;
border: none;
color: #858585;
font-size: 20px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.15s;
}
.modal-close:hover {
background: #37373d;
color: #ffffff;
}
.modal-body {
padding: 20px;
}
.modal-body p {
margin: 0 0 16px 0;
color: #cccccc;
font-size: 14px;
}
.input-label {
display: block;
margin-bottom: 8px;
color: #cccccc;
font-size: 13px;
font-weight: 500;
}
.input-field {
width: 100%;
padding: 10px 12px;
background: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 4px;
color: #ffffff;
font-size: 14px;
font-family: 'Consolas', 'Monaco', monospace;
transition: all 0.2s;
}
.input-field:focus {
outline: none;
border-color: #007acc;
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
}
.input-error {
margin-top: 8px;
color: #f48771;
font-size: 12px;
}
.delete-target {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #1e1e1e;
border: 1px solid #be1100;
border-radius: 4px;
margin: 12px 0;
}
.delete-target strong {
color: #f48771;
font-size: 14px;
}
.warning-text {
color: #f48771;
font-size: 12px;
}
.modal-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
padding: 16px 20px;
border-top: 1px solid #3e3e42;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #0e639c;
color: #ffffff;
}
.btn-primary:hover:not(:disabled) {
background: #1177bb;
transform: translateY(-1px);
}
.btn-secondary {
background: #3c3c3c;
color: #cccccc;
border: 1px solid #3e3e42;
}
.btn-secondary:hover {
background: #4e4e52;
}
.btn-danger {
background: #be1100;
color: #ffffff;
}
.btn-danger:hover {
background: #f14c2e;
transform: translateY(-1px);
}
/* File Tree */
.file-tree {
display: flex;
flex-direction: column;
}
.folder-item {
margin: 8px 0;
}
.folder-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: #252526;
border-left: 3px solid #007acc;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.folder-header:hover {
background: #2a2d2e;
}
.folder-icon {
font-size: 14px;
}
.folder-name {
flex: 1;
font-size: 13px;
font-weight: 600;
color: #4fc3f7;
}
.file-count {
font-size: 11px;
color: #858585;
background: #1e1e1e;
padding: 2px 6px;
border-radius: 10px;
}
.file-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 4px;
font-size: 13px;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #2a2d2e;
}
.file-item.active {
background-color: #37373d;
color: #ffffff;
}
.file-item.in-folder {
margin-left: 20px;
border-left: 2px solid #3e3e42;
padding-left: 12px;
}
.file-icon {
font-size: 14px;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #858585;
}
.empty-state p {
margin: 8px 0;
font-size: 13px;
}
.empty-hint {
font-size: 11px;
color: #6a6a6a;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
<template>
<div class="project-selector-overlay">
<div class="project-selector-modal">
<div class="modal-header">
<h2>🚀 Selecciona el Directorio del Proyecto Amayo</h2>
<p>Elige la carpeta raíz donde está el proyecto del bot</p>
</div>
<div class="modal-content">
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div class="path-display">
<div class="path-label">Ruta actual:</div>
<div class="path-value">{{ currentPath || 'No seleccionada' }}</div>
</div>
<div class="actions">
<button @click="selectDirectory" class="select-btn">
📁 Seleccionar Directorio
</button>
<button
v-if="currentPath"
@click="validateAndSave"
class="save-btn"
:disabled="validating"
>
{{ validating ? '⏳ Validando...' : '✅ Usar esta Ruta' }}
</button>
</div>
<div class="info-box">
<p><strong> Requisitos:</strong></p>
<ul>
<li>El directorio debe contener <code>src/commands/</code></li>
<li>El directorio debe contener <code>src/events/</code></li>
<li>Compatible con Windows, Linux y macOS</li>
</ul>
</div>
<div v-if="savedPath" class="saved-path">
<p><strong>📌 Última ruta guardada:</strong></p>
<p class="saved-value">{{ savedPath }}</p>
<button @click="useSavedPath" class="use-saved-btn">
Usar Ruta Guardada
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { open } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core';
const emit = defineEmits<{
'path-selected': [path: string];
}>();
const currentPath = ref<string>('');
const savedPath = ref<string>('');
const errorMessage = ref<string>('');
const validating = ref(false);
// Cargar ruta guardada
onMounted(() => {
const saved = localStorage.getItem('amayo-project-path');
if (saved) {
savedPath.value = saved;
}
});
// Seleccionar directorio
async function selectDirectory() {
try {
errorMessage.value = '';
const selected = await open({
directory: true,
multiple: false,
title: 'Selecciona el directorio raíz del proyecto Amayo',
});
if (selected && typeof selected === 'string') {
currentPath.value = selected;
}
} catch (error: any) {
errorMessage.value = `Error al seleccionar directorio: ${error.message}`;
console.error('Error:', error);
}
}
// Validar y guardar
async function validateAndSave() {
if (!currentPath.value) {
errorMessage.value = 'Por favor selecciona un directorio primero';
return;
}
validating.value = true;
errorMessage.value = '';
try {
const isValid = await invoke<boolean>('validate_project_path', {
path: currentPath.value,
});
if (isValid) {
// Guardar en localStorage
localStorage.setItem('amayo-project-path', currentPath.value);
// Emitir evento
emit('path-selected', currentPath.value);
} else {
errorMessage.value =
'El directorio seleccionado no es válido. ' +
'Asegúrate de que contiene las carpetas src/commands/ y src/events/';
}
} catch (error: any) {
errorMessage.value = `Error validando directorio: ${error}`;
console.error('Error:', error);
} finally {
validating.value = false;
}
}
// Usar ruta guardada
async function useSavedPath() {
currentPath.value = savedPath.value;
await validateAndSave();
}
</script>
<style scoped>
.project-selector-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.project-selector-modal {
background-color: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 8px;
width: 90%;
max-width: 600px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.modal-header {
padding: 24px;
border-bottom: 1px solid #3e3e42;
background-color: #2d2d30;
border-radius: 8px 8px 0 0;
}
.modal-header h2 {
margin: 0 0 8px 0;
font-size: 20px;
color: #ffffff;
}
.modal-header p {
margin: 0;
color: #cccccc;
font-size: 14px;
}
.modal-content {
padding: 24px;
}
.error-message {
padding: 12px 16px;
background-color: #f14c4c;
color: #ffffff;
border-radius: 4px;
margin-bottom: 20px;
font-size: 13px;
}
.path-display {
background-color: #252526;
padding: 16px;
border-radius: 4px;
margin-bottom: 20px;
}
.path-label {
font-size: 12px;
color: #858585;
margin-bottom: 8px;
font-weight: 600;
}
.path-value {
font-size: 13px;
color: #4ec9b0;
font-family: 'Consolas', 'Monaco', monospace;
word-break: break-all;
}
.actions {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.select-btn,
.save-btn {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
}
.select-btn {
background-color: #3e3e42;
color: #ffffff;
}
.select-btn:hover {
background-color: #4e4e52;
}
.save-btn {
background-color: #0e639c;
color: #ffffff;
}
.save-btn:hover:not(:disabled) {
background-color: #1177bb;
}
.save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-box {
background-color: #252526;
padding: 16px;
border-left: 3px solid #0e639c;
border-radius: 4px;
margin-bottom: 20px;
}
.info-box p {
margin: 0 0 12px 0;
color: #ffffff;
font-size: 13px;
}
.info-box ul {
margin: 0;
padding-left: 20px;
}
.info-box li {
color: #cccccc;
font-size: 13px;
margin-bottom: 8px;
}
.info-box code {
background-color: #3e3e42;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
color: #4ec9b0;
}
.saved-path {
background-color: #2d2d30;
padding: 16px;
border-radius: 4px;
border: 1px solid #3e3e42;
}
.saved-path p {
margin: 0 0 8px 0;
color: #cccccc;
font-size: 13px;
}
.saved-path p:last-of-type {
margin-bottom: 12px;
}
.saved-value {
color: #4ec9b0 !important;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px !important;
word-break: break-all;
}
.use-saved-btn {
width: 100%;
padding: 8px 12px;
background-color: #3e3e42;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
}
.use-saved-btn:hover {
background-color: #0e639c;
}
</style>

View File

@@ -0,0 +1,684 @@
<template>
<div class="sidebar">
<div class="sidebar-header">
<div class="header-title">
<h2>Amayo Bot Editor</h2>
</div>
<div class="header-actions">
<button @click="emit('toggle-database')" class="icon-btn" title="Base de Datos">
🗄
</button>
<button
@click="emit('toggle-dev-ultra')"
:class="['icon-btn', 'dev-ultra-btn', { active: devUltraMode }]"
title="Modo Dev Ultra"
>
</button>
<button @click="emit('change-directory')" class="icon-btn" title="Cambiar directorio">
📁
</button>
</div>
</div>
<div class="project-path">
<span class="path-label">Proyecto:</span>
<span class="path-value">{{ truncatePath(projectRoot) }}</span>
</div>
<div class="actions-section">
<button @click="emit('new-command')" class="action-btn primary">
Nuevo Comando
</button>
<button @click="emit('new-event')" class="action-btn primary">
Nuevo Evento
</button>
<button @click="emit('toggle-env-manager')" class="action-btn env-manager">
🔐 Variables ENV
</button>
<button @click="emit('refresh')" class="action-btn secondary">
🔄 Refrescar
</button>
</div>
<div class="files-section">
<!-- Sección de Comandos -->
<div class="section-group">
<div class="section-header" @click="toggleSection('commands')">
<span class="section-icon">{{ sectionsExpanded.commands ? '📂' : '📁' }}</span>
<h3 class="section-title">Comandos</h3>
<span class="section-count">{{ stats.totalCommands }}</span>
</div>
<div v-if="sectionsExpanded.commands" class="section-content">
<!-- Subsección Comandos Mensaje -->
<div class="subsection">
<div class="subsection-header" @click="toggleSubsection('messageCommands')">
<span class="subsection-icon">📝</span>
<span class="subsection-title">Comandos Mensaje</span>
<span class="subsection-count">{{ stats.messageCommands }}</span>
</div>
<FileExplorer
v-if="subsectionsExpanded.messageCommands"
:files="props.commands.filter(c => c.commandType === 'message')"
:selected-file="selectedFile"
:project-root="projectRoot"
base-path="commands"
@select-file="(file) => emit('select-file', file)"
@refresh="emit('refresh')"
@notify="(msg, type) => emit('notify', msg, type)"
/>
</div>
<!-- Subsección Comandos Slash -->
<div class="subsection">
<div class="subsection-header" @click="toggleSubsection('slashCommands')">
<span class="subsection-icon">⚡</span>
<span class="subsection-title">Comandos Slash</span>
<span class="subsection-count">{{ stats.slashCommands }}</span>
</div>
<FileExplorer
v-if="subsectionsExpanded.slashCommands"
:files="props.commands.filter(c => c.commandType === 'slash')"
:selected-file="selectedFile"
:project-root="projectRoot"
base-path="commands"
@select-file="(file) => emit('select-file', file)"
@refresh="emit('refresh')"
@notify="(msg, type) => emit('notify', msg, type)"
/>
</div>
</div>
</div>
<!-- Sección de Eventos -->
<div class="section-group">
<div class="section-header" @click="toggleSection('events')">
<span class="section-icon">{{ sectionsExpanded.events ? '📂' : '📁' }}</span>
<h3 class="section-title">Eventos</h3>
<span class="section-count">{{ stats.totalEvents }}</span>
</div>
<div v-if="sectionsExpanded.events" class="section-content">
<!-- Subsección Eventos Estándar -->
<div class="subsection">
<div class="subsection-header" @click="toggleSubsection('standardEvents')">
<span class="subsection-icon">🎯</span>
<span class="subsection-title">Eventos Estándar</span>
<span class="subsection-count">{{ stats.standardEvents }}</span>
</div>
<FileExplorer
v-if="subsectionsExpanded.standardEvents"
:files="props.events.filter(e => e.eventType === 'standard')"
:selected-file="selectedFile"
:project-root="projectRoot"
base-path="events"
@select-file="(file) => emit('select-file', file)"
@refresh="emit('refresh')"
@notify="(msg, type) => emit('notify', msg, type)"
/>
</div>
<!-- Subsección Eventos Custom -->
<div class="subsection">
<div class="subsection-header" @click="toggleSubsection('customEvents')">
<span class="subsection-icon">✨</span>
<span class="subsection-title">Eventos Custom</span>
<span class="subsection-count">{{ stats.customEvents }}</span>
</div>
<FileExplorer
v-if="subsectionsExpanded.customEvents"
:files="props.events.filter(e => e.eventType === 'extra')"
:selected-file="selectedFile"
:project-root="projectRoot"
base-path="events"
@select-file="(file) => emit('select-file', file)"
@refresh="emit('refresh')"
@notify="(msg, type) => emit('notify', msg, type)"
/>
</div>
</div>
</div>
<!-- Sección Ultra Mode: Todos los archivos del proyecto -->
<div v-if="devUltraMode" class="section-group ultra-mode">
<div class="section-header" @click="toggleSection('allFiles')">
<span class="section-icon">{{ sectionsExpanded.allFiles ? '📂' : '📁' }}</span>
<h3 class="section-title">⚡ Archivos del Proyecto</h3>
<span class="section-count">{{ allFiles.length }}</span>
</div>
<FileExplorer
v-if="sectionsExpanded.allFiles"
:files="allFiles"
:selected-file="selectedFile"
:project-root="projectRoot"
base-path=""
@select-file="(file) => emit('select-file', file)"
@refresh="emit('refresh')"
@notify="(msg, type) => emit('notify', msg, type)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import FileExplorer from './FileExplorer.vue';
import type { ProjectStats, FileInfo } from '../types/bot';
const props = defineProps<{
stats: ProjectStats;
commands: FileInfo[];
events: FileInfo[];
allFiles: FileInfo[];
selectedFile: FileInfo | null;
projectRoot: string;
devUltraMode?: boolean;
}>();
const emit = defineEmits<{
'new-command': [];
'new-event': [];
'refresh': [];
'select-file': [file: FileInfo];
'change-directory': [];
'toggle-dev-ultra': [];
'toggle-database': [];
'toggle-env-manager': [];
'notify': [message: string, type: 'success' | 'error' | 'info'];
}>();
// Estado de expansión de secciones
const sectionsExpanded = ref({
commands: true,
events: true,
allFiles: false
});
const subsectionsExpanded = ref({
messageCommands: true,
slashCommands: true,
standardEvents: true,
customEvents: true
});
function toggleSection(section: 'commands' | 'events' | 'allFiles') {
sectionsExpanded.value[section] = !sectionsExpanded.value[section];
}
function toggleSubsection(subsection: keyof typeof subsectionsExpanded.value) {
subsectionsExpanded.value[subsection] = !subsectionsExpanded.value[subsection];
}
function truncatePath(path: string): string {
if (!path) return 'No seleccionado';
const parts = path.split(/[/\\]/);
if (parts.length > 3) {
return '.../' + parts.slice(-2).join('/');
}
return path;
}
</script>
<style scoped>
.sidebar {
width: 300px;
background-color: #252526;
color: #cccccc;
height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
border-right: 1px solid #3e3e42;
}
.sidebar-header {
padding: 12px 16px;
background-color: #2d2d30;
border-bottom: 1px solid #3e3e42;
}
.header-title h2 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #ffffff;
}
.header-actions {
display: flex;
gap: 6px;
}
.icon-btn {
background: #3c3c3c;
border: 1px solid #3e3e42;
font-size: 16px;
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.2s;
flex: 1;
}
.icon-btn:hover {
background-color: #4e4e52;
transform: translateY(-1px);
}
.dev-ultra-btn {
position: relative;
}
.dev-ultra-btn.active {
background-color: #0e639c;
border-color: #1177bb;
box-shadow: 0 0 10px rgba(14, 99, 156, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 10px rgba(14, 99, 156, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(14, 99, 156, 0.8);
}
}
.project-path {
padding: 8px 16px;
background-color: #1e1e1e;
border-bottom: 1px solid #3e3e42;
font-size: 11px;
}
.path-label {
color: #858585;
margin-right: 6px;
}
.path-value {
color: #4ec9b0;
font-family: 'Consolas', 'Monaco', monospace;
}
.actions-section {
padding: 12px;
background-color: #1e1e1e;
border-bottom: 1px solid #3e3e42;
display: flex;
flex-direction: column;
gap: 8px;
}
.action-btn {
padding: 10px 14px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
text-align: left;
}
.action-btn.primary {
background-color: #0e639c;
color: #ffffff;
}
.action-btn.primary:hover {
background-color: #1177bb;
transform: translateY(-1px);
}
.action-btn.secondary {
background-color: #3c3c3c;
color: #cccccc;
border: 1px solid #3e3e42;
}
.action-btn.secondary:hover {
background-color: #4e4e52;
}
.action-btn.env-manager {
background-color: #4ec9b0;
color: #1e1e1e;
}
.action-btn.env-manager:hover {
background-color: #5fd4bf;
transform: translateY(-1px);
}
.files-section {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.section-group {
margin-bottom: 8px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
cursor: pointer;
transition: background-color 0.2s;
user-select: none;
}
.section-header:hover {
background-color: #2d2d30;
}
.section-icon {
font-size: 16px;
}
.section-title {
flex: 1;
margin: 0;
font-size: 14px;
font-weight: 600;
color: #ffffff;
}
.section-count {
background-color: #3c3c3c;
color: #cccccc;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.section-content {
padding-left: 8px;
}
.subsection {
margin-bottom: 4px;
}
.subsection-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s;
user-select: none;
}
.subsection-header:hover {
background-color: #2d2d30;
}
.subsection-icon {
font-size: 14px;
}
.subsection-title {
flex: 1;
font-size: 13px;
color: #cccccc;
font-weight: 500;
}
.subsection-count {
background-color: #3c3c3c;
color: #858585;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
.file-list {
padding-left: 12px;
}
.file-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
cursor: pointer;
transition: background-color 0.2s;
border-left: 2px solid transparent;
}
.file-item:hover {
background-color: #2d2d30;
}
.file-item.active {
background-color: #3e3e42;
border-left-color: #0e639c;
}
.file-icon {
font-size: 13px;
opacity: 0.8;
}
.file-name {
font-size: 12px;
color: #cccccc;
font-family: 'Consolas', 'Monaco', monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.file-path {
font-size: 10px;
color: #858585;
font-family: 'Consolas', 'Monaco', monospace;
margin-left: auto;
padding-left: 8px;
}
.ultra-mode {
border: 2px solid #0e639c;
border-radius: 8px;
margin-top: 8px;
background: linear-gradient(135deg, rgba(14, 99, 156, 0.1) 0%, rgba(14, 99, 156, 0.05) 100%);
}
.ultra-mode .section-header {
background-color: rgba(14, 99, 156, 0.2);
}
.empty-list {
padding: 8px 16px;
font-size: 11px;
color: #858585;
font-style: italic;
}
.sidebar::-webkit-scrollbar {
width: 10px;
}
.sidebar::-webkit-scrollbar-track {
background: #1e1e1e;
}
.sidebar::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 5px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: #4e4e52;
}
.sidebar .section-title {
background-color: #2d2d30;
font-weight: 600;
color: #4ec9b0;
}
.stat-label {
color: #cccccc;
}
.stat-value {
color: #4ec9b0;
font-weight: 600;
}
.stat-divider {
height: 1px;
background-color: #3e3e42;
margin: 8px 0;
}
.actions-section {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
border-bottom: 1px solid #3e3e42;
}
.action-btn {
padding: 8px 12px;
background-color: #0e639c;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
}
.action-btn:hover {
background-color: #1177bb;
}
.action-btn.secondary {
background-color: #3e3e42;
}
.action-btn.secondary:hover {
background-color: #4e4e52;
}
.files-section {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.section-title {
padding: 8px 16px;
position: sticky;
top: 0;
background-color: #252526;
z-index: 1;
}
.section-title h3 {
margin: 0;
font-size: 13px;
color: #cccccc;
font-weight: 600;
}
.file-list {
padding: 0 8px;
}
.file-item {
display: flex;
align-items: center;
padding: 6px 8px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 4px;
font-size: 13px;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #2a2d2e;
}
.file-item.active {
background-color: #37373d;
color: #ffffff;
}
.file-icon {
margin-right: 8px;
font-size: 14px;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Estilos para carpetas jerárquicas */
.folder-group {
margin: 8px 0;
}
.folder-header {
display: flex;
align-items: center;
padding: 6px 8px;
margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
color: #cccccc;
background-color: #252526;
border-left: 3px solid #007acc;
border-radius: 3px;
}
.file-item.in-folder {
margin-left: 20px;
border-left: 2px solid #3e3e42;
padding-left: 12px;
}
.file-item.in-folder:hover {
border-left-color: #007acc;
}
/* Scrollbar personalizado */
.sidebar::-webkit-scrollbar,
.files-section::-webkit-scrollbar {
width: 10px;
}
.sidebar::-webkit-scrollbar-track,
.files-section::-webkit-scrollbar-track {
background: #1e1e1e;
}
.sidebar::-webkit-scrollbar-thumb,
.files-section::-webkit-scrollbar-thumb {
background: #424242;
border-radius: 5px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.files-section::-webkit-scrollbar-thumb:hover {
background: #4e4e4e;
}
</style>

View File

@@ -0,0 +1,189 @@
<template>
<div class="skeleton-container">
<div class="skeleton-sidebar">
<div class="skeleton-header">
<div class="skeleton-logo"></div>
<div class="skeleton-title"></div>
</div>
<div class="skeleton-stats">
<div class="skeleton-stat" v-for="i in 4" :key="i">
<div class="skeleton-stat-icon"></div>
<div class="skeleton-stat-text"></div>
</div>
</div>
<div class="skeleton-buttons">
<div class="skeleton-button"></div>
<div class="skeleton-button"></div>
</div>
<div class="skeleton-list">
<div class="skeleton-list-item" v-for="i in 8" :key="i">
<div class="skeleton-item-icon"></div>
<div class="skeleton-item-text"></div>
</div>
</div>
</div>
<div class="skeleton-main">
<div class="skeleton-editor-header"></div>
<div class="skeleton-editor-content">
<div class="skeleton-code-line" v-for="i in 20" :key="i" :style="{ width: `${60 + Math.random() * 35}%` }"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.skeleton-container {
display: flex;
height: 100vh;
background-color: #1e1e1e;
overflow: hidden;
}
.skeleton-sidebar {
width: 280px;
background-color: #252526;
border-right: 1px solid #3e3e42;
padding: 16px;
}
.skeleton-header {
margin-bottom: 24px;
}
.skeleton-logo {
width: 40px;
height: 40px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
margin-bottom: 12px;
}
.skeleton-title {
width: 180px;
height: 24px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-stats {
margin-bottom: 24px;
}
.skeleton-stat {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.skeleton-stat-icon {
width: 20px;
height: 20px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-stat-text {
flex: 1;
height: 16px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-buttons {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.skeleton-button {
flex: 1;
height: 36px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-list {
margin-top: 16px;
}
.skeleton-list-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.skeleton-item-icon {
width: 16px;
height: 16px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 3px;
}
.skeleton-item-text {
flex: 1;
height: 14px;
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-main {
flex: 1;
display: flex;
flex-direction: column;
background-color: #1e1e1e;
}
.skeleton-editor-header {
height: 40px;
background: linear-gradient(90deg, #252526 25%, #2d2d30 50%, #252526 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-bottom: 1px solid #3e3e42;
}
.skeleton-editor-content {
flex: 1;
padding: 20px;
}
.skeleton-code-line {
height: 18px;
margin-bottom: 10px;
background: linear-gradient(90deg, #252526 25%, #2d2d30 50%, #252526 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 3px;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

4
AEditor/src/main.ts Normal file
View File

@@ -0,0 +1,4 @@
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

564
AEditor/src/monaco-types/discord.d.ts vendored Normal file
View File

@@ -0,0 +1,564 @@
// Declaraciones básicas de Discord.js para autocompletado en Monaco Editor
declare module "discord.js" {
// Client
export class Client {
constructor(options: ClientOptions);
login(token: string): Promise<string>;
destroy(): void;
user: ClientUser | null;
guilds: GuildManager;
channels: ChannelManager;
users: UserManager;
on<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this;
once<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this;
}
export interface ClientOptions {
intents: GatewayIntentBits[];
}
export enum GatewayIntentBits {
Guilds = 1 << 0,
GuildMembers = 1 << 1,
GuildBans = 1 << 2,
GuildEmojisAndStickers = 1 << 3,
GuildIntegrations = 1 << 4,
GuildWebhooks = 1 << 5,
GuildInvites = 1 << 6,
GuildVoiceStates = 1 << 7,
GuildPresences = 1 << 8,
GuildMessages = 1 << 9,
GuildMessageReactions = 1 << 10,
GuildMessageTyping = 1 << 11,
DirectMessages = 1 << 12,
DirectMessageReactions = 1 << 13,
DirectMessageTyping = 1 << 14,
MessageContent = 1 << 15,
GuildScheduledEvents = 1 << 16,
AutoModerationConfiguration = 1 << 20,
AutoModerationExecution = 1 << 21,
}
// User & ClientUser
export class User {
id: string;
username: string;
discriminator: string;
avatar: string | null;
bot: boolean;
tag: string;
displayAvatarURL(options?: ImageURLOptions): string;
send(options: MessageCreateOptions): Promise<Message>;
}
export class ClientUser extends User {
setActivity(name: string, options?: ActivityOptions): Presence;
setStatus(status: PresenceStatusData): Presence;
setPresence(presence: PresenceData): Presence;
}
// Message
export class Message {
id: string;
content: string;
author: User;
channel: TextChannel | DMChannel;
guild: Guild | null;
member: GuildMember | null;
createdTimestamp: number;
createdAt: Date;
attachments: Collection<string, Attachment>;
embeds: Embed[];
mentions: MessageMentions;
reply(options: MessageReplyOptions): Promise<Message>;
delete(): Promise<Message>;
edit(options: MessageEditOptions): Promise<Message>;
react(emoji: string): Promise<MessageReaction>;
}
export interface MessageCreateOptions {
content?: string;
embeds?: EmbedBuilder[];
components?: ActionRowBuilder<any>[];
files?: AttachmentBuilder[];
ephemeral?: boolean;
}
export interface MessageReplyOptions extends MessageCreateOptions {}
export interface MessageEditOptions extends MessageCreateOptions {}
// Guild
export class Guild {
id: string;
name: string;
icon: string | null;
ownerId: string;
memberCount: number;
channels: GuildChannelManager;
members: GuildMemberManager;
roles: RoleManager;
emojis: GuildEmojiManager;
leave(): Promise<Guild>;
}
// GuildMember
export class GuildMember {
id: string;
user: User;
nickname: string | null;
displayName: string;
roles: GuildMemberRoleManager;
permissions: PermissionsBitField;
joinedTimestamp: number | null;
kick(reason?: string): Promise<GuildMember>;
ban(options?: BanOptions): Promise<GuildMember>;
timeout(duration: number, reason?: string): Promise<GuildMember>;
send(options: MessageCreateOptions): Promise<Message>;
}
export interface BanOptions {
reason?: string;
deleteMessageDays?: number;
}
// Interaction
export class Interaction {
id: string;
type: InteractionType;
user: User;
guild: Guild | null;
channel: TextChannel | null;
member: GuildMember | null;
isButton(): this is ButtonInteraction;
isChatInputCommand(): this is ChatInputCommandInteraction;
isStringSelectMenu(): this is StringSelectMenuInteraction;
isModalSubmit(): this is ModalSubmitInteraction;
}
export class ChatInputCommandInteraction extends Interaction {
commandName: string;
options: CommandInteractionOptionResolver;
reply(options: InteractionReplyOptions): Promise<void>;
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
editReply(options: InteractionEditReplyOptions): Promise<Message>;
followUp(options: InteractionReplyOptions): Promise<Message>;
deleteReply(): Promise<void>;
}
export class ButtonInteraction extends Interaction {
customId: string;
reply(options: InteractionReplyOptions): Promise<void>;
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
update(options: InteractionUpdateOptions): Promise<void>;
}
export class StringSelectMenuInteraction extends Interaction {
customId: string;
values: string[];
reply(options: InteractionReplyOptions): Promise<void>;
update(options: InteractionUpdateOptions): Promise<void>;
}
export class ModalSubmitInteraction extends Interaction {
customId: string;
fields: ModalSubmitFields;
reply(options: InteractionReplyOptions): Promise<void>;
}
export interface InteractionReplyOptions {
content?: string;
embeds?: EmbedBuilder[];
components?: ActionRowBuilder<any>[];
ephemeral?: boolean;
files?: AttachmentBuilder[];
}
export interface InteractionDeferReplyOptions {
ephemeral?: boolean;
}
export interface InteractionEditReplyOptions
extends InteractionReplyOptions {}
export interface InteractionUpdateOptions extends InteractionReplyOptions {}
// Embed
export class EmbedBuilder {
constructor(data?: object);
setTitle(title: string): this;
setDescription(description: string): this;
setColor(color: string | number): this;
setAuthor(options: EmbedAuthorOptions): this;
setFooter(options: EmbedFooterOptions): this;
setImage(url: string): this;
setThumbnail(url: string): this;
setTimestamp(timestamp?: Date | number): this;
setURL(url: string): this;
addFields(...fields: EmbedField[]): this;
}
export interface EmbedAuthorOptions {
name: string;
iconURL?: string;
url?: string;
}
export interface EmbedFooterOptions {
text: string;
iconURL?: string;
}
export interface EmbedField {
name: string;
value: string;
inline?: boolean;
}
// Buttons
export class ButtonBuilder {
constructor(data?: object);
setCustomId(customId: string): this;
setLabel(label: string): this;
setStyle(style: ButtonStyle): this;
setEmoji(emoji: string): this;
setURL(url: string): this;
setDisabled(disabled: boolean): this;
}
export enum ButtonStyle {
Primary = 1,
Secondary = 2,
Success = 3,
Danger = 4,
Link = 5,
}
// Select Menus
export class StringSelectMenuBuilder {
constructor(data?: object);
setCustomId(customId: string): this;
setPlaceholder(placeholder: string): this;
addOptions(...options: StringSelectMenuOptionBuilder[]): this;
setMinValues(minValues: number): this;
setMaxValues(maxValues: number): this;
setDisabled(disabled: boolean): this;
}
export class StringSelectMenuOptionBuilder {
constructor(data?: object);
setLabel(label: string): this;
setValue(value: string): this;
setDescription(description: string): this;
setEmoji(emoji: string): this;
setDefault(isDefault: boolean): this;
}
// Modal
export class ModalBuilder {
constructor(data?: object);
setCustomId(customId: string): this;
setTitle(title: string): this;
addComponents(...components: ActionRowBuilder<TextInputBuilder>[]): this;
}
export class TextInputBuilder {
constructor(data?: object);
setCustomId(customId: string): this;
setLabel(label: string): this;
setStyle(style: TextInputStyle): this;
setPlaceholder(placeholder: string): this;
setValue(value: string): this;
setRequired(required: boolean): this;
setMinLength(minLength: number): this;
setMaxLength(maxLength: number): this;
}
export enum TextInputStyle {
Short = 1,
Paragraph = 2,
}
// Action Row
export class ActionRowBuilder<T> {
constructor(data?: object);
addComponents(...components: T[]): this;
}
// Attachment
export class AttachmentBuilder {
constructor(
attachment: Buffer | string,
options?: AttachmentBuilderOptions
);
setName(name: string): this;
setDescription(description: string): this;
}
export interface AttachmentBuilderOptions {
name?: string;
description?: string;
}
// Permissions
export class PermissionsBitField {
has(permission: bigint | string, checkAdmin?: boolean): boolean;
add(...permissions: bigint[]): this;
remove(...permissions: bigint[]): this;
}
export enum PermissionFlagsBits {
CreateInstantInvite = 1n << 0n,
KickMembers = 1n << 1n,
BanMembers = 1n << 2n,
Administrator = 1n << 3n,
ManageChannels = 1n << 4n,
ManageGuild = 1n << 5n,
AddReactions = 1n << 6n,
ViewAuditLog = 1n << 7n,
ViewChannel = 1n << 10n,
SendMessages = 1n << 11n,
ManageMessages = 1n << 13n,
EmbedLinks = 1n << 14n,
AttachFiles = 1n << 15n,
ReadMessageHistory = 1n << 16n,
MentionEveryone = 1n << 17n,
Connect = 1n << 20n,
Speak = 1n << 21n,
MuteMembers = 1n << 22n,
DeafenMembers = 1n << 23n,
MoveMembers = 1n << 24n,
ManageRoles = 1n << 28n,
}
// Channels
export class TextChannel {
id: string;
name: string;
type: ChannelType;
guild: Guild;
send(options: MessageCreateOptions): Promise<Message>;
bulkDelete(
messages: number | Message[]
): Promise<Collection<string, Message>>;
}
export enum ChannelType {
GuildText = 0,
DM = 1,
GuildVoice = 2,
GuildCategory = 4,
GuildAnnouncement = 5,
GuildStageVoice = 13,
GuildForum = 15,
}
// Managers
export class GuildManager {
cache: Collection<string, Guild>;
fetch(id: string): Promise<Guild>;
}
export class ChannelManager {
cache: Collection<string, any>;
fetch(id: string): Promise<any>;
}
export class UserManager {
cache: Collection<string, User>;
fetch(id: string): Promise<User>;
}
export class GuildChannelManager {
cache: Collection<string, any>;
create(options: GuildChannelCreateOptions): Promise<any>;
}
export interface GuildChannelCreateOptions {
name: string;
type?: ChannelType;
topic?: string;
parent?: string;
}
export class GuildMemberManager {
cache: Collection<string, GuildMember>;
fetch(id: string): Promise<GuildMember>;
ban(user: string, options?: BanOptions): Promise<void>;
unban(user: string, reason?: string): Promise<void>;
}
export class RoleManager {
cache: Collection<string, Role>;
create(options: RoleCreateOptions): Promise<Role>;
}
export interface RoleCreateOptions {
name?: string;
color?: string | number;
permissions?: bigint[];
hoist?: boolean;
mentionable?: boolean;
}
export class GuildMemberRoleManager {
cache: Collection<string, Role>;
add(role: string | Role, reason?: string): Promise<GuildMember>;
remove(role: string | Role, reason?: string): Promise<GuildMember>;
}
export class GuildEmojiManager {
cache: Collection<string, GuildEmoji>;
create(options: GuildEmojiCreateOptions): Promise<GuildEmoji>;
}
// Collection
export class Collection<K, V> extends Map<K, V> {
filter(
fn: (value: V, key: K, collection: this) => boolean
): Collection<K, V>;
map<T>(fn: (value: V, key: K, collection: this) => T): T[];
find(fn: (value: V, key: K, collection: this) => boolean): V | undefined;
first(): V | undefined;
first(amount: number): V[];
random(): V | undefined;
}
// Events
export interface ClientEvents {
ready: [client: Client];
messageCreate: [message: Message];
messageDelete: [message: Message];
messageUpdate: [oldMessage: Message, newMessage: Message];
interactionCreate: [interaction: Interaction];
guildMemberAdd: [member: GuildMember];
guildMemberRemove: [member: GuildMember];
guildCreate: [guild: Guild];
guildDelete: [guild: Guild];
}
// Otros tipos
export class Role {
id: string;
name: string;
color: number;
permissions: PermissionsBitField;
}
export class GuildEmoji {
id: string;
name: string;
animated: boolean;
}
export interface GuildEmojiCreateOptions {
attachment: Buffer | string;
name: string;
}
export class MessageMentions {
users: Collection<string, User>;
members: Collection<string, GuildMember>;
channels: Collection<string, any>;
roles: Collection<string, Role>;
}
export class MessageReaction {
emoji: GuildEmoji | string;
count: number;
me: boolean;
}
export class Attachment {
id: string;
name: string;
url: string;
size: number;
}
export class Embed {
title?: string;
description?: string;
url?: string;
color?: number;
timestamp?: string;
fields: EmbedField[];
}
export class DMChannel {
id: string;
type: ChannelType;
recipient: User;
send(options: MessageCreateOptions): Promise<Message>;
}
export class CommandInteractionOptionResolver {
getString(name: string, required?: boolean): string | null;
getInteger(name: string, required?: boolean): number | null;
getBoolean(name: string, required?: boolean): boolean | null;
getUser(name: string, required?: boolean): User | null;
getMember(name: string): GuildMember | null;
getChannel(name: string): any | null;
getRole(name: string): Role | null;
}
export class ModalSubmitFields {
getTextInputValue(customId: string): string;
}
export enum InteractionType {
Ping = 1,
ApplicationCommand = 2,
MessageComponent = 3,
ApplicationCommandAutocomplete = 4,
ModalSubmit = 5,
}
export enum ApplicationCommandOptionType {
SubCommand = 1,
SubCommandGroup = 2,
String = 3,
Integer = 4,
Boolean = 5,
User = 6,
Channel = 7,
Role = 8,
Mentionable = 9,
Number = 10,
Attachment = 11,
}
export interface ActivityOptions {
type?: ActivityType;
}
export enum ActivityType {
Playing = 0,
Streaming = 1,
Listening = 2,
Watching = 3,
Competing = 5,
}
export type PresenceStatusData = "online" | "idle" | "dnd" | "invisible";
export interface PresenceData {
status?: PresenceStatusData;
activities?: ActivityOptions[];
}
export class Presence {
status: PresenceStatusData;
activities: ActivityOptions[];
}
export interface ImageURLOptions {
format?: "png" | "jpg" | "jpeg" | "webp" | "gif";
size?: 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
}
}

50
AEditor/src/types/bot.ts Normal file
View File

@@ -0,0 +1,50 @@
// Tipos que mapean la estructura del bot de Discord
export interface CommandMessage {
name: string;
type: "message";
aliases?: string[];
cooldown?: number;
description?: string;
category?: string;
usage?: string;
run: string; // Código de la función run como string
}
export interface CommandSlash {
name: string;
description: string;
type: "slash";
options?: any[];
cooldown?: number;
run: string; // Código de la función run como string
}
export type Command = CommandMessage | CommandSlash;
export interface Event {
name: string;
type: "standard" | "extra";
eventName?: string; // Para eventos estándar (ready, messageCreate, etc.)
path: string;
code: string;
}
export interface ProjectStats {
messageCommands: number;
slashCommands: number;
standardEvents: number;
customEvents: number;
totalCommands: number;
totalEvents: number;
}
export interface FileInfo {
name: string;
path: string;
relative_path: string;
type: "command" | "event";
commandType?: "message" | "slash";
eventType?: "standard" | "extra";
folder?: string;
}

7
AEditor/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}