Skip to content

Updated the actual inputs value and not their initial value in views.update #132

@mister-good-deal

Description

@mister-good-deal

Hi, I'm using your lib and it is working fine except for one case.

When I am loading modal inputs data from an existing model, the returned modal View correctly shows the loaded values in the GUI. But after submitting the modal, the payload has undefined values for the ones being loaded before.

I am using initialValue or initialOption to load the values from my model on TextInput or StaticSelect.

It is like I need to fill a "real" input value and not its initial value to fix my issue.

Do you know how can I deal with this?

Here is some code sample of my app

src/listeners/views/assistant-configuration-modal.ts

import logger from "../../logger/winston.ts";
import prisma from "../../prisma/client.ts";
import { functionMap } from "../../ai/functions-mapper.ts";
import { createPublicChannel } from "../../slack/assistants-channel.ts";
import { GptModel, MistralModel, AIServiceName } from "@prisma/client";
import { Modal, Blocks, TextInput, StaticMultiSelect, StaticSelect, Bits, Md } from "slack-block-builder";

import type { AllMiddlewareArgs, SlackViewMiddlewareArgs, ViewOutput, ViewErrorsResponseAction } from "@slack/bolt";
import type { WebClient } from "@slack/bolt/node_modules/@slack/web-api"; // @fixme: update to @slack/web-api when https://github.com/slackapi/bolt-js/pull/2036 is merged
import type { Assistant } from "@prisma/client";

type ViewEventArgs = AllMiddlewareArgs & SlackViewMiddlewareArgs;

interface ModalInputs {
    assistantId: number;
    name: string;
    description: string;
    instructions: string;
    slackChannelName: string;
    aiServiceName: AIServiceName;
    model?: string;
    functions: string[];
}

export const NONE_SELECTED = -1;

export async function assistantConfigurationModal(slackTeamId: string, inputs?: ModalInputs) {
    const newAssistantLabel = "Create new assistant";
    const assistantsList = await prisma.assistant.findMany({ where: { SlackTeamId: slackTeamId } });
    const assistantsOptions = [Bits.Option({ text: newAssistantLabel, value: NONE_SELECTED.toString() })];
    // Populate the assistant options with the existing assistants
    assistantsOptions.push(
        ...assistantsList.map(assistant =>
            Bits.Option({
                text: assistant.name,
                value: assistant.id.toString()
            })
        )
    );

    const functionOptions = Object.keys(functionMap).map(functionName =>
        Bits.Option({
            text: functionMap[functionName].attributes.name,
            value: functionName
        })
    );

    const assistantId = inputs?.assistantId ?? NONE_SELECTED;
    const assistantName = inputs?.name ?? newAssistantLabel;
    const aiServiceName = inputs?.aiServiceName ?? AIServiceName.OPENAI;
    const model = inputs?.model ?? getDefaultModelValue(aiServiceName);

    return Modal()
        .callbackId("assistantConfigurationModalCallback")
        .title("AI Assistant")
        .submit("Submit")
        .close("Cancel")
        .blocks(
            Blocks.Section({ text: "Create an AI assistant bound to a public channel." }),
            Blocks.Divider(),
            Blocks.Input({ label: "Create or edit Assistant" })
                .element(
                    StaticSelect({ placeholder: "Select assistant" })
                        .options(assistantsOptions)
                        .initialOption(Bits.Option({ text: assistantName, value: assistantId.toString() }))
                        .actionId("select_assistant_action")
                )
                .dispatchAction()
                .blockId("select_assistant_action_block_id"),
            Blocks.Input({ label: "Assistant name" })
                .element(
                    TextInput({ placeholder: "Enter the assistant name" })
                        .initialValue(inputs?.name ?? "")
                        .actionId("assistant_name_text_input_action")
                )
                .blockId("assistant_name_block_id"),
            Blocks.Input({ label: "Assistant description" })
                .element(
                    TextInput({ placeholder: "Enter the assistant description", multiline: true })
                        .initialValue(inputs?.description ?? "")
                        .actionId("assistant_description_text_input_action")
                )
                .blockId("assistant_description_block_id"),
            Blocks.Input({ label: "Assistant instructions" })
                .element(
                    TextInput({ placeholder: "Enter the assistant instructions", multiline: true })
                        .initialValue(inputs?.instructions ?? "")
                        .actionId("assistant_instructions_text_input_action")
                )
                .blockId("assistant_instructions_block_id"),
            Blocks.Input({ label: "Public channel name" })
                .element(
                    TextInput({ placeholder: "Enter the public channel name" })
                        .initialValue(inputs?.slackChannelName ?? "")
                        .actionId("assistant_channel_name_text_input_action")
                )
                .blockId("assistant_channel_name_block_id"),
            Blocks.Input({ label: "AI service" })
                .element(
                    StaticSelect({ placeholder: "Select AI service" })
                        .actionId("select_ai_service_action")
                        .options(
                            Object.values(AIServiceName).map(service => Bits.Option({ text: service, value: service }))
                        )
                        .initialOption(Bits.Option({ text: aiServiceName, value: aiServiceName }))
                )
                .blockId("select_ai_service_block_id")
                .dispatchAction(),
            Blocks.Input({ label: "Model" })
                .element(
                    StaticSelect({ placeholder: "Select AI model" })
                        .actionId("select_ai_model_action")
                        .options(
                            Object.values(getModelType(aiServiceName)).map(model =>
                                Bits.Option({ text: model, value: model })
                            )
                        )
                        .initialOption(Bits.Option({ text: model, value: model }))
                )
                .blockId("select_ai_model_block_id"),
            Blocks.Section({
                text: `_See different models purpose and features in ${getModelLinkDescription(aiServiceName)}_`
            }),
            Blocks.Input({ label: "Functions" })
                .element(
                    StaticMultiSelect({ placeholder: "Select the functions assistant can use" })
                        .actionId("select_assistant_functions_action")
                        .options(functionOptions)
                        .initialOptions(inputs?.functions.map(value => Bits.Option({ text: value, value })) ?? [])
                )
                .blockId("select_assistant_functions_block_id"),
            Blocks.Section({
                text: "_The selected functions will be available for the assistant._"
            })
        )
        .buildToObject();
}

export function loadModalInputsFromView(view: ViewOutput): ModalInputs {
    const assistantId = parseInt(
        view.state.values.select_assistant_action_block_id.select_assistant_action.selected_option!.value!
    );
    const name = view.state.values.assistant_name_block_id.assistant_name_text_input_action.value!;
    const description = view.state.values.assistant_description_block_id.assistant_description_text_input_action.value!;
    const instructions =
        view.state.values.assistant_instructions_block_id.assistant_instructions_text_input_action.value!;
    const channel = view.state.values.assistant_channel_name_block_id.assistant_channel_name_text_input_action.value!;
    const ai = view.state.values.select_ai_service_block_id.select_ai_service_action.selected_option!.value!;
    const model = view.state.values.select_ai_model_block_id.select_ai_model_action.selected_option!.value!;
    const functions =
        view.state.values.select_assistant_functions_block_id.select_assistant_functions_action.selected_options!.map(
            (option: any) => option.value
        );
    logger.debug("view.state.values", view.state.values);
    return {
        assistantId,
        name,
        description,
        instructions,
        slackChannelName: channel,
        aiServiceName: ai as AIServiceName,
        model,
        functions
    };
}

// ...

src/listeners/actions/assistant-configuration-modal-actions-callback.ts

import logger from "../../logger/winston.ts";
import prisma from "../../prisma/client.ts";
import { feedback } from "../../utilities/slack.ts";
import {
    assistantConfigurationModal,
    loadModalInputsFromView,
    loadModalInputsFromAssistant,
    getDefaultModelValue,
    NONE_SELECTED
} from "../views/assistant-configuration-modal.ts";

import type { Assistant } from "@prisma/client";
import type { AllMiddlewareArgs, BlockAction, SlackActionMiddlewareArgs, StaticSelectAction } from "@slack/bolt";

type ActionEventArgs = AllMiddlewareArgs & SlackActionMiddlewareArgs<BlockAction>;

export async function selectAiServiceActionCallback({ ack, body, client }: ActionEventArgs) {
    try {
        await ack();
        // Type guard for typescript completion
        if (body.type !== "block_actions" || !body.view) return;

        const userInputs = loadModalInputsFromView(body.view);
        // Reset the model value
        userInputs.model = getDefaultModelValue(userInputs.aiServiceName);
        // @todo - not working ofc, body is not forwared outside of the function but this is kind of what we want to do
        body.view.state.values.select_ai_model_block_id.select_ai_model_action.value = getDefaultModelValue(
            userInputs.aiServiceName
        );
        // Update the configuration modal with the selected AI service values and the user's inputs
        logger.debug("userInputs", userInputs);
        logger.debug("assistantConfigurationModal", await assistantConfigurationModal(body.user.team_id!, userInputs));
        await client.views.update({
            user_id: body.user.id,
            view_id: body.view.id,
            hash: body.view.hash,
            view: await assistantConfigurationModal(body.user.team_id!, userInputs)
        });
    } catch (error) {
        logger.error(error);
        await feedback(client, body, "An error occurred while selecting the AI service. Please try again.");
    }
}

async function loadAssistant(assistantId: number, teamId: string): Promise<Assistant | undefined> {
    if (assistantId === NONE_SELECTED) return undefined;
    return await prisma.assistant.findUniqueOrThrow({ where: { id: assistantId, SlackTeamId: teamId } });
}

export async function selectAssistantActionCallback({ ack, body, client, payload }: ActionEventArgs) {
    try {
        await ack();
        // Type guard for typescript completion
        if (body.type !== "block_actions" || !body.view) return;

        const payloadTyped = payload as StaticSelectAction;
        const assistantId = parseInt(payloadTyped.selected_option.value);
        // Update the configuration modal with the selected assistant values
        await client.views.update({
            user_id: body.user.id,
            view_id: body.view.id,
            hash: body.view.hash,
            view: await assistantConfigurationModal(
                body.user.team_id!,
                loadModalInputsFromAssistant(await loadAssistant(assistantId, body.user.team_id!))
            )
        });
    } catch (error) {
        logger.error(error);
        await feedback(client, body, "An error occurred while selecting the assistant. Please try again.");
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions