ClientX/_opt/responsesPrompt.js

161 lines
6.6 KiB
JavaScript
Raw Normal View History

import { MessageFlags } from 'discord-api-types/v10';
2025-05-02 16:45:36 +00:00
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({ flags: MessageFlags.Ephemeral});
2025-05-02 16:45:36 +00:00
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.', flags: MessageFlags.Ephemeral});
2025-05-02 16:45:36 +00:00
}
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.', flags: MessageFlags.Ephemeral});
2025-05-02 16:45:36 +00:00
} 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));
});
2025-05-03 17:27:21 +00:00
// Add one extra empty field to allow expansion (up to MAX_FIELDS)
if (chunks.length < MAX_FIELDS) {
const i = chunks.length;
2025-05-02 16:45:36 +00:00
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!', flags: MessageFlags.Ephemeral});
2025-05-02 16:45:36 +00:00
} catch (err) {
client.logger.error(`responsesPrompt modal submit error: ${err.message}`);
await interaction.reply({ content: `Error saving prompt: ${err.message}`, ephemeral: true });
}
});
}