159 lines
6.4 KiB
JavaScript
159 lines
6.4 KiB
JavaScript
|
|
import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js';
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
import fetch from 'node-fetch';
|
||
|
|
// Modal text input limits
|
||
|
|
const MAX_LEN = 4000;
|
||
|
|
const MAX_FIELDS = 5;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* responsesPrompt module
|
||
|
|
* Provides a /prompt command to view or update the AI response prompt
|
||
|
|
* Stored in PocketBase collection 'responses_prompts' with fields:
|
||
|
|
* clientId (string), prompt (text), updatedBy (string), created/updated timestamps
|
||
|
|
*/
|
||
|
|
export const commands = [
|
||
|
|
{
|
||
|
|
data: new SlashCommandBuilder()
|
||
|
|
.setName('prompt')
|
||
|
|
.setDescription('View or update the AI response prompt')
|
||
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||
|
|
.setDMPermission(false)
|
||
|
|
.addStringOption(opt =>
|
||
|
|
opt.setName('url')
|
||
|
|
.setDescription('URL to a .txt file containing the prompt')
|
||
|
|
.setRequired(false)
|
||
|
|
),
|
||
|
|
async execute(interaction, client) {
|
||
|
|
const url = interaction.options.getString('url');
|
||
|
|
const clientId = client.config.id;
|
||
|
|
// URL-based update
|
||
|
|
if (url) {
|
||
|
|
client.logger.info(`[cmd:prompt] URL update requested for client ${clientId}: ${url}`);
|
||
|
|
await interaction.deferReply({ ephemeral: true });
|
||
|
|
if (!url.toLowerCase().endsWith('.txt')) {
|
||
|
|
client.logger.warn(`[cmd:prompt] Invalid URL extension, must end .txt: ${url}`);
|
||
|
|
return interaction.editReply({ content: 'URL must point to a .txt file.', ephemeral: true });
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const res = await fetch(url);
|
||
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
|
|
const text = await res.text();
|
||
|
|
// Upsert into PocketBase
|
||
|
|
const existing = await client.pb.getFirst('responses_prompts', `clientId="${clientId}"`);
|
||
|
|
const recId = existing?.id;
|
||
|
|
await client.pb.upsert('responses_prompts', recId, {
|
||
|
|
clientId,
|
||
|
|
prompt: text,
|
||
|
|
updatedBy: interaction.user.id
|
||
|
|
});
|
||
|
|
client.responsesPrompt = text;
|
||
|
|
return interaction.editReply({ content: 'Prompt updated from URL.', ephemeral: true });
|
||
|
|
} catch (err) {
|
||
|
|
client.logger.error(`[cmd:prompt] URL update failed: ${err.message}`);
|
||
|
|
return interaction.editReply({ content: `Error fetching URL: ${err.message}`, ephemeral: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Modal-based edit
|
||
|
|
const existingPrompt = client.responsesPrompt || '';
|
||
|
|
// Prevent modal if prompt exceeds capacity
|
||
|
|
if (existingPrompt.length > MAX_LEN * MAX_FIELDS) {
|
||
|
|
return interaction.reply({
|
||
|
|
content: 'Current prompt too large for modal editing (exceeds 20000 chars); please use URL.',
|
||
|
|
ephemeral: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// Prepare chunks
|
||
|
|
const chunks = [];
|
||
|
|
for (let offset = 0; offset < existingPrompt.length; offset += MAX_LEN) {
|
||
|
|
chunks.push(existingPrompt.slice(offset, offset + MAX_LEN));
|
||
|
|
}
|
||
|
|
// Build modal
|
||
|
|
const modal = new ModalBuilder()
|
||
|
|
.setCustomId(`promptModal-${clientId}`)
|
||
|
|
.setTitle('Edit AI Prompt');
|
||
|
|
// Add text inputs for existing chunks
|
||
|
|
chunks.forEach((chunk, idx) => {
|
||
|
|
const input = new TextInputBuilder()
|
||
|
|
.setCustomId(`prompt_${idx}`)
|
||
|
|
.setLabel(`Part ${idx + 1}`)
|
||
|
|
.setStyle(TextInputStyle.Paragraph)
|
||
|
|
.setRequired(idx === 0)
|
||
|
|
.setMaxLength(MAX_LEN)
|
||
|
|
.setValue(chunk);
|
||
|
|
modal.addComponents(new ActionRowBuilder().addComponents(input));
|
||
|
|
});
|
||
|
|
// Add empty remaining fields
|
||
|
|
for (let i = chunks.length; i < MAX_FIELDS; i++) {
|
||
|
|
const input = new TextInputBuilder()
|
||
|
|
.setCustomId(`prompt_${i}`)
|
||
|
|
.setLabel(`Part ${i + 1}`)
|
||
|
|
.setStyle(TextInputStyle.Paragraph)
|
||
|
|
.setRequired(false)
|
||
|
|
.setMaxLength(MAX_LEN);
|
||
|
|
modal.addComponents(new ActionRowBuilder().addComponents(input));
|
||
|
|
}
|
||
|
|
client.logger.info(`[cmd:prompt] Showing modal editor for client ${clientId} with ${chunks.length} parts`);
|
||
|
|
await interaction.showModal(modal);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
/** track clients for modal handling */
|
||
|
|
const _clients = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* init hook: load existing prompt and register modal submit handler
|
||
|
|
*/
|
||
|
|
export async function init(client, clientConfig) {
|
||
|
|
client.logger.info('[module:responsesPrompt] Module loaded');
|
||
|
|
const clientId = clientConfig.id;
|
||
|
|
// Load prompt from PocketBase or fallback to file
|
||
|
|
let prompt = '';
|
||
|
|
try {
|
||
|
|
const record = await client.pb.getFirst('responses_prompts', `clientId="${clientId}"`);
|
||
|
|
if (record && record.prompt) {
|
||
|
|
prompt = record.prompt;
|
||
|
|
} else if (clientConfig.responses?.systemPromptPath) {
|
||
|
|
const filePath = path.isAbsolute(clientConfig.responses.systemPromptPath)
|
||
|
|
? clientConfig.responses.systemPromptPath
|
||
|
|
: path.join(process.cwd(), clientConfig.responses.systemPromptPath);
|
||
|
|
prompt = fs.readFileSync(filePath, 'utf8');
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
client.logger.error(`responsesPrompt init error: ${err.message}`);
|
||
|
|
}
|
||
|
|
client.responsesPrompt = prompt;
|
||
|
|
_clients.push({ client, clientConfig });
|
||
|
|
// Modal submit listener
|
||
|
|
client.on('interactionCreate', async interaction => {
|
||
|
|
if (!interaction.isModalSubmit()) return;
|
||
|
|
if (interaction.customId !== `promptModal-${clientId}`) return;
|
||
|
|
// Reassemble prompt from modal fields, inserting line breaks between parts
|
||
|
|
client.logger.info(`[cmd:prompt] Modal submission received for client ${clientId}`);
|
||
|
|
const parts = [];
|
||
|
|
for (let i = 0; i < MAX_FIELDS; i++) {
|
||
|
|
try {
|
||
|
|
const value = interaction.fields.getTextInputValue(`prompt_${i}`) || '';
|
||
|
|
if (value.trim().length > 0) parts.push(value);
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
const newPrompt = parts.join('\n');
|
||
|
|
// Save to PocketBase
|
||
|
|
try {
|
||
|
|
client.logger.debug(`[cmd:prompt] Saving new prompt for client ${clientId}, length=${newPrompt.length}`);
|
||
|
|
const existing = await client.pb.getFirst('responses_prompts', `clientId="${clientId}"`);
|
||
|
|
const recId = existing?.id;
|
||
|
|
await client.pb.upsert('responses_prompts', recId, {
|
||
|
|
clientId,
|
||
|
|
prompt: newPrompt,
|
||
|
|
updatedBy: interaction.user.id
|
||
|
|
});
|
||
|
|
client.responsesPrompt = newPrompt;
|
||
|
|
await interaction.reply({ content: 'Prompt updated!', ephemeral: true });
|
||
|
|
} catch (err) {
|
||
|
|
client.logger.error(`responsesPrompt modal submit error: ${err.message}`);
|
||
|
|
await interaction.reply({ content: `Error saving prompt: ${err.message}`, ephemeral: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|