add default slack channel config

This commit is contained in:
pablodanswer 2025-02-05 14:26:26 -08:00
parent 78153e5012
commit 7153cb09f1
24 changed files with 658 additions and 349 deletions

View File

@ -0,0 +1,76 @@
"""add default slack channel config
Revision ID: eaa3b5593925
Revises: 98a5008d8711
Create Date: 2025-02-03 18:07:56.552526
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "eaa3b5593925"
down_revision = "98a5008d8711"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Add is_default column
op.add_column(
"slack_channel_config",
sa.Column("is_default", sa.Boolean(), nullable=False, server_default="false"),
)
op.create_index(
"ix_slack_channel_config_slack_bot_id_default",
"slack_channel_config",
["slack_bot_id", "is_default"],
unique=True,
postgresql_where=sa.text("is_default IS TRUE"),
)
# Create default channel configs for existing slack bots without one
conn = op.get_bind()
slack_bots = conn.execute(sa.text("SELECT id FROM slack_bot")).fetchall()
for slack_bot in slack_bots:
slack_bot_id = slack_bot[0]
existing_default = conn.execute(
sa.text(
"SELECT id FROM slack_channel_config WHERE slack_bot_id = :bot_id AND is_default = TRUE"
),
{"bot_id": slack_bot_id},
).fetchone()
if not existing_default:
conn.execute(
sa.text(
"""
INSERT INTO slack_channel_config (
slack_bot_id, persona_id, channel_config, enable_auto_filters, is_default
) VALUES (
:bot_id, NULL,
'{"channel_name": null, "respond_member_group_list": [], "answer_filters": [], "follow_up_tags": []}',
FALSE, TRUE
)
"""
),
{"bot_id": slack_bot_id},
)
def downgrade() -> None:
# Delete default slack channel configs
conn = op.get_bind()
conn.execute(sa.text("DELETE FROM slack_channel_config WHERE is_default = TRUE"))
# Remove index
op.drop_index(
"ix_slack_channel_config_slack_bot_id_default",
table_name="slack_channel_config",
)
# Remove is_default column
op.drop_column("slack_channel_config", "is_default")

View File

@ -80,7 +80,7 @@ def oneoff_standard_answers(
def _handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_channel_config: SlackChannelConfig | None,
slack_channel_config: SlackChannelConfig,
prompt: Prompt | None,
logger: OnyxLoggingAdapter,
client: WebClient,
@ -94,13 +94,10 @@ def _handle_standard_answers(
Returns True if standard answers are found to match the user's message and therefore,
we still need to respond to the users.
"""
# if no channel config, then no standard answers are configured
if not slack_channel_config:
return False
slack_thread_id = message_info.thread_to_respond
configured_standard_answer_categories = (
slack_channel_config.standard_answer_categories if slack_channel_config else []
slack_channel_config.standard_answer_categories
)
configured_standard_answers = set(
[

View File

@ -1 +1,2 @@
SLACK_BOT_PERSONA_PREFIX = "__slack_bot_persona__"
DEFAULT_PERSONA_SLACK_CHANNEL_NAME = "DEFAULT_SLACK_CHANNEL"

View File

@ -1716,7 +1716,7 @@ class ChannelConfig(TypedDict):
"""NOTE: is a `TypedDict` so it can be used as a type hint for a JSONB column
in Postgres"""
channel_name: str
channel_name: str | None # None for default channel config
respond_tag_only: NotRequired[bool] # defaults to False
respond_to_bots: NotRequired[bool] # defaults to False
respond_member_group_list: NotRequired[list[str]]
@ -1737,7 +1737,6 @@ class SlackChannelConfig(Base):
persona_id: Mapped[int | None] = mapped_column(
ForeignKey("persona.id"), nullable=True
)
# JSON for flexibility. Contains things like: channel name, team members, etc.
channel_config: Mapped[ChannelConfig] = mapped_column(
postgresql.JSONB(), nullable=False
)
@ -1746,6 +1745,8 @@ class SlackChannelConfig(Base):
Boolean, nullable=False, default=False
)
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
persona: Mapped[Persona | None] = relationship("Persona")
slack_bot: Mapped["SlackBot"] = relationship(
"SlackBot",
@ -1757,6 +1758,21 @@ class SlackChannelConfig(Base):
back_populates="slack_channel_configs",
)
__table_args__ = (
UniqueConstraint(
"slack_bot_id",
"is_default",
name="uq_slack_channel_config_slack_bot_id_default",
),
Index(
"ix_slack_channel_config_slack_bot_id_default",
"slack_bot_id",
"is_default",
unique=True,
postgresql_where=(is_default is True), # type: ignore
),
)
class SlackBot(Base):
__tablename__ = "slack_bot"

View File

@ -74,3 +74,15 @@ def remove_slack_bot(
def fetch_slack_bots(db_session: Session) -> Sequence[SlackBot]:
return db_session.scalars(select(SlackBot)).all()
def fetch_slack_bot_tokens(
db_session: Session, slack_bot_id: int
) -> dict[str, str] | None:
slack_bot = db_session.scalar(select(SlackBot).where(SlackBot.id == slack_bot_id))
if not slack_bot:
return None
return {
"app_token": slack_bot.app_token,
"bot_token": slack_bot.bot_token,
}

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from onyx.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT
from onyx.context.search.enums import RecencyBiasSetting
from onyx.db.constants import DEFAULT_PERSONA_SLACK_CHANNEL_NAME
from onyx.db.constants import SLACK_BOT_PERSONA_PREFIX
from onyx.db.models import ChannelConfig
from onyx.db.models import Persona
@ -22,8 +23,8 @@ from onyx.utils.variable_functionality import (
)
def _build_persona_name(channel_name: str) -> str:
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name}"
def _build_persona_name(channel_name: str | None) -> str:
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name if channel_name else DEFAULT_PERSONA_SLACK_CHANNEL_NAME}"
def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
@ -40,7 +41,7 @@ def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
def create_slack_channel_persona(
db_session: Session,
channel_name: str,
channel_name: str | None,
document_set_ids: list[int],
existing_persona_id: int | None = None,
num_chunks: float = MAX_CHUNKS_FED_TO_CHAT,
@ -90,6 +91,7 @@ def insert_slack_channel_config(
channel_config: ChannelConfig,
standard_answer_category_ids: list[int],
enable_auto_filters: bool,
is_default: bool = False,
) -> SlackChannelConfig:
versioned_fetch_standard_answer_categories_by_ids = (
fetch_versioned_implementation_with_fallback(
@ -115,12 +117,26 @@ def insert_slack_channel_config(
f"Some or all categories with ids {standard_answer_category_ids} do not exist"
)
if is_default:
existing_default = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,
SlackChannelConfig.is_default is True, # type: ignore
)
)
if existing_default:
raise ValueError("A default config already exists for this Slack bot.")
else:
if "channel_name" not in channel_config:
raise ValueError("Channel name is required for non-default configs.")
slack_channel_config = SlackChannelConfig(
slack_bot_id=slack_bot_id,
persona_id=persona_id,
channel_config=channel_config,
standard_answer_categories=existing_standard_answer_categories,
enable_auto_filters=enable_auto_filters,
is_default=is_default,
)
db_session.add(slack_channel_config)
db_session.commit()
@ -164,12 +180,7 @@ def update_slack_channel_config(
f"Some or all categories with ids {standard_answer_category_ids} do not exist"
)
# get the existing persona id before updating the object
existing_persona_id = slack_channel_config.persona_id
# update the config
# NOTE: need to do this before cleaning up the old persona or else we
# will encounter `violates foreign key constraint` errors
slack_channel_config.persona_id = persona_id
slack_channel_config.channel_config = channel_config
slack_channel_config.standard_answer_categories = list(
@ -177,20 +188,6 @@ def update_slack_channel_config(
)
slack_channel_config.enable_auto_filters = enable_auto_filters
# if the persona has changed, then clean up the old persona
if persona_id != existing_persona_id and existing_persona_id:
existing_persona = db_session.scalar(
select(Persona).where(Persona.id == existing_persona_id)
)
# if the existing persona was one created just for use with this Slack channel,
# then clean it up
if existing_persona and existing_persona.name.startswith(
SLACK_BOT_PERSONA_PREFIX
):
_cleanup_relationships(
db_session=db_session, persona_id=existing_persona_id
)
db_session.commit()
return slack_channel_config
@ -253,3 +250,32 @@ def fetch_slack_channel_config(
SlackChannelConfig.id == slack_channel_config_id
)
)
def fetch_slack_channel_config_for_channel_or_default(
db_session: Session, slack_bot_id: int, channel_name: str | None
) -> SlackChannelConfig | None:
# attempt to find channel-specific config first
if channel_name:
sc_config = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,
SlackChannelConfig.channel_config["channel_name"].astext
== channel_name,
)
)
else:
sc_config = None
if sc_config:
return sc_config
# if none found, see if there is a default
default_sc = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,
SlackChannelConfig.is_default == True, # noqa: E712
)
)
return default_sc

View File

@ -3,9 +3,11 @@ import os
from sqlalchemy.orm import Session
from onyx.db.models import SlackChannelConfig
from onyx.db.slack_channel_config import (
fetch_slack_channel_config_for_channel_or_default,
)
from onyx.db.slack_channel_config import fetch_slack_channel_configs
VALID_SLACK_FILTERS = [
"answerable_prefilter",
"well_answered_postfilter",
@ -17,18 +19,16 @@ def get_slack_channel_config_for_bot_and_channel(
db_session: Session,
slack_bot_id: int,
channel_name: str | None,
) -> SlackChannelConfig | None:
if not channel_name:
return None
slack_bot_configs = fetch_slack_channel_configs(
db_session=db_session, slack_bot_id=slack_bot_id
) -> SlackChannelConfig:
slack_bot_config = fetch_slack_channel_config_for_channel_or_default(
db_session=db_session, slack_bot_id=slack_bot_id, channel_name=channel_name
)
for config in slack_bot_configs:
if channel_name in config.channel_config["channel_name"]:
return config
if not slack_bot_config:
raise ValueError(
"No default configuration has been set for this Slack bot. This should not be possible."
)
return None
return slack_bot_config
def validate_channel_name(

View File

@ -106,7 +106,7 @@ def remove_scheduled_feedback_reminder(
def handle_message(
message_info: SlackMessageInfo,
slack_channel_config: SlackChannelConfig | None,
slack_channel_config: SlackChannelConfig,
client: WebClient,
feedback_reminder_id: str | None,
tenant_id: str | None,

View File

@ -64,7 +64,7 @@ def rate_limits(
def handle_regular_answer(
message_info: SlackMessageInfo,
slack_channel_config: SlackChannelConfig | None,
slack_channel_config: SlackChannelConfig,
receiver_ids: list[str] | None,
client: WebClient,
channel: str,
@ -76,7 +76,7 @@ def handle_regular_answer(
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
) -> bool:
channel_conf = slack_channel_config.channel_config if slack_channel_config else None
channel_conf = slack_channel_config.channel_config
messages = message_info.thread_messages
@ -92,7 +92,7 @@ def handle_regular_answer(
prompt = None
# If no persona is specified, use the default search based persona
# This way slack flow always has a persona
persona = slack_channel_config.persona if slack_channel_config else None
persona = slack_channel_config.persona
if not persona:
with get_session_with_tenant(tenant_id) as db_session:
persona = get_persona_by_id(DEFAULT_PERSONA_ID, user, db_session)
@ -134,11 +134,7 @@ def handle_regular_answer(
single_message_history = slackify_message_thread(history_messages) or None
bypass_acl = False
if (
slack_channel_config
and slack_channel_config.persona
and slack_channel_config.persona.document_sets
):
if slack_channel_config.persona and slack_channel_config.persona.document_sets:
# For Slack channels, use the full document set, admin will be warned when configuring it
# with non-public document sets
bypass_acl = True
@ -190,11 +186,7 @@ def handle_regular_answer(
# auto_detect_filters = (
# persona.llm_filter_extraction if persona is not None else True
# )
auto_detect_filters = (
slack_channel_config.enable_auto_filters
if slack_channel_config is not None
else False
)
auto_detect_filters = slack_channel_config.enable_auto_filters
retrieval_details = RetrievalDetails(
run_search=OptionalSearchSetting.ALWAYS,
real_time=False,

View File

@ -14,7 +14,7 @@ logger = setup_logger()
def handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_channel_config: SlackChannelConfig | None,
slack_channel_config: SlackChannelConfig,
prompt: Prompt | None,
logger: OnyxLoggingAdapter,
client: WebClient,
@ -40,7 +40,7 @@ def handle_standard_answers(
def _handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_channel_config: SlackChannelConfig | None,
slack_channel_config: SlackChannelConfig,
prompt: Prompt | None,
logger: OnyxLoggingAdapter,
client: WebClient,

View File

@ -790,8 +790,7 @@ def process_message(
# Be careful about this default, don't want to accidentally spam every channel
# Users should be able to DM slack bot in their private channels though
if (
slack_channel_config is None
and not respond_every_channel
not respond_every_channel
# Can't have configs for DMs so don't toss them out
and not is_dm
# If /OnyxBot (is_bot_msg) or @OnyxBot (bypass_filters)
@ -801,8 +800,7 @@ def process_message(
return
follow_up = bool(
slack_channel_config
and slack_channel_config.channel_config
slack_channel_config.channel_config
and slack_channel_config.channel_config.get("follow_up_tags")
is not None
)

View File

@ -215,6 +215,7 @@ class SlackChannelConfig(BaseModel):
# XXX this is going away soon
standard_answer_categories: list[StandardAnswerCategory]
enable_auto_filters: bool
is_default: bool
@classmethod
def from_model(
@ -237,6 +238,7 @@ class SlackChannelConfig(BaseModel):
for standard_answer_category_model in slack_channel_config_model.standard_answer_categories
],
enable_auto_filters=slack_channel_config_model.enable_auto_filters,
is_default=slack_channel_config_model.is_default,
)
@ -279,3 +281,8 @@ class AllUsersResponse(BaseModel):
accepted_pages: int
invited_pages: int
slack_users_pages: int
class SlackChannel(BaseModel):
id: str
name: str

View File

@ -1,6 +1,10 @@
from typing import Any
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
@ -12,6 +16,7 @@ from onyx.db.models import ChannelConfig
from onyx.db.models import User
from onyx.db.persona import get_persona_by_id
from onyx.db.slack_bot import fetch_slack_bot
from onyx.db.slack_bot import fetch_slack_bot_tokens
from onyx.db.slack_bot import fetch_slack_bots
from onyx.db.slack_bot import insert_slack_bot
from onyx.db.slack_bot import remove_slack_bot
@ -25,6 +30,7 @@ from onyx.db.slack_channel_config import update_slack_channel_config
from onyx.onyxbot.slack.config import validate_channel_name
from onyx.server.manage.models import SlackBot
from onyx.server.manage.models import SlackBotCreationRequest
from onyx.server.manage.models import SlackChannel
from onyx.server.manage.models import SlackChannelConfig
from onyx.server.manage.models import SlackChannelConfigCreationRequest
from onyx.server.manage.validate_tokens import validate_app_token
@ -48,12 +54,6 @@ def _form_channel_config(
answer_filters = slack_channel_config_creation_request.answer_filters
follow_up_tags = slack_channel_config_creation_request.follow_up_tags
if not raw_channel_name:
raise HTTPException(
status_code=400,
detail="Must provide at least one channel name",
)
try:
cleaned_channel_name = validate_channel_name(
db_session=db_session,
@ -108,6 +108,12 @@ def create_slack_channel_config(
current_slack_channel_config_id=None,
)
if channel_config["channel_name"] is None:
raise HTTPException(
status_code=400,
detail="Channel name is required",
)
persona_id = None
if slack_channel_config_creation_request.persona_id is not None:
persona_id = slack_channel_config_creation_request.persona_id
@ -120,11 +126,11 @@ def create_slack_channel_config(
).id
slack_channel_config_model = insert_slack_channel_config(
db_session=db_session,
slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
persona_id=persona_id,
channel_config=channel_config,
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
db_session=db_session,
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
)
return SlackChannelConfig.from_model(slack_channel_config_model)
@ -235,6 +241,23 @@ def create_bot(
app_token=slack_bot_creation_request.app_token,
)
# Create a default Slack channel config
default_channel_config = ChannelConfig(
channel_name=None,
respond_member_group_list=[],
answer_filters=[],
follow_up_tags=[],
)
insert_slack_channel_config(
db_session=db_session,
slack_bot_id=slack_bot_model.id,
persona_id=None,
channel_config=default_channel_config,
standard_answer_category_ids=[],
enable_auto_filters=False,
is_default=True,
)
create_milestone_and_report(
user=None,
distinct_id=tenant_id or "N/A",
@ -315,3 +338,48 @@ def list_bot_configs(
SlackChannelConfig.from_model(slack_bot_config_model)
for slack_bot_config_model in slack_bot_config_models
]
@router.get(
"/admin/slack-app/bots/{bot_id}/channels",
)
def get_all_channels_from_slack_api(
bot_id: int,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> list[SlackChannel]:
tokens = fetch_slack_bot_tokens(db_session, bot_id)
if not tokens or "bot_token" not in tokens:
raise HTTPException(
status_code=404, detail="Bot token not found for the given bot ID"
)
bot_token = tokens["bot_token"]
client = WebClient(token=bot_token)
try:
channels = []
cursor = None
while True:
response = client.conversations_list(
types="public_channel,private_channel",
exclude_archived=True,
limit=1000,
cursor=cursor,
)
for channel in response["channels"]:
channels.append(SlackChannel(id=channel["id"], name=channel["name"]))
response_metadata: dict[str, Any] = response.get("response_metadata", {})
if isinstance(response_metadata, dict):
cursor = response_metadata.get("next_cursor")
if not cursor:
break
else:
break
return channels
except SlackApiError as e:
raise HTTPException(
status_code=500, detail=f"Error fetching channels from Slack API: {str(e)}"
)

View File

@ -1,10 +1,9 @@
"use client";
import { PageSelector } from "@/components/PageSelector";
import { SlackBot } from "@/lib/types";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { FiCheck, FiEdit, FiXCircle } from "react-icons/fi";
import { FiEdit } from "react-icons/fi";
import {
Table,
TableBody,
@ -13,6 +12,8 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { SlackBot } from "@/lib/types";
const NUM_IN_PAGE = 20;
@ -42,7 +43,7 @@ function ClickableTableRow({
);
}
export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
const [page, setPage] = useState(1);
// sort by id for consistent ordering
@ -67,8 +68,9 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Default Config</TableHead>
<TableHead>Channel Count</TableHead>
<TableHead>Enabled</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -85,21 +87,27 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
{slackBot.name}
</div>
</TableCell>
<TableCell>{slackBot.configs_count}</TableCell>
<TableCell>
{slackBot.enabled ? (
<FiCheck className="text-emerald-600" size="18" />
<Badge variant="success">Enabled</Badge>
) : (
<FiXCircle className="text-red-600" size="18" />
<Badge variant="destructive">Disabled</Badge>
)}
</TableCell>
<TableCell>
<Badge variant="secondary">Default Set</Badge>
</TableCell>
<TableCell>{slackBot.configs_count}</TableCell>
<TableCell>
{/* Add any action buttons here if needed */}
</TableCell>
</ClickableTableRow>
);
})}
{slackBots.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
colSpan={5}
className="text-center text-muted-foreground"
>
Please add a New Slack Bot to begin chatting with Danswer!
@ -128,4 +136,4 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
)}
</div>
);
}
};

View File

@ -7,6 +7,7 @@ import { createSlackBot, updateSlackBot } from "./new/lib";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { useEffect } from "react";
import { Switch } from "@/components/ui/switch";
export const SlackTokensForm = ({
isUpdate,
@ -33,7 +34,9 @@ export const SlackTokensForm = ({
return (
<Formik
initialValues={initialValues}
initialValues={{
...initialValues,
}}
validationSchema={Yup.object().shape({
bot_token: Yup.string().required(),
app_token: Yup.string().required(),

View File

@ -14,8 +14,10 @@ import {
} from "@/components/ui/table";
import Link from "next/link";
import { useState } from "react";
import { FiArrowUpRight } from "react-icons/fi";
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { FiPlusSquare, FiSettings } from "react-icons/fi";
const numToDisplay = 50;
@ -32,128 +34,147 @@ export function SlackChannelConfigsTable({
}) {
const [page, setPage] = useState(1);
// sort by name for consistent ordering
slackChannelConfigs.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
const defaultConfig = slackChannelConfigs.find((config) => config.is_default);
const channelConfigs = slackChannelConfigs.filter(
(config) => !config.is_default
);
return (
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Channel</TableHead>
<TableHead>Assistant</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackChannelConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackChannelConfig) => {
return (
<TableRow
key={slackChannelConfig.id}
className="cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`;
}}
>
<TableCell>
<div className="flex gap-x-2">
<div className="my-auto">
<EditIcon />
</div>
<div className="my-auto">
{"#" + slackChannelConfig.channel_config.channel_name}
</div>
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
{slackChannelConfig.persona &&
!isPersonaASlackBotPersona(slackChannelConfig.persona) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
className="text-blue-500 flex hover:underline"
>
{slackChannelConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
<div>
{slackChannelConfig.persona &&
slackChannelConfig.persona.document_sets.length > 0
? slackChannelConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<div
className="cursor-pointer hover:text-destructive"
onClick={async (e) => {
e.stopPropagation();
const response = await deleteSlackChannelConfig(
slackChannelConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackChannelConfig.id}" deleted`,
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete Slack bot config - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
>
<TrashIcon />
</div>
</TableCell>
</TableRow>
);
})}
{/* Empty row with message when table has no data */}
{slackChannelConfigs.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
Please add a New Slack Bot Configuration to begin chatting
with Onyx!
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<div className="space-y-8">
<div className="flex justify-between items-center mb-6">
<Button
variant="outline"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${defaultConfig?.id}`;
}}
>
<FiSettings />
Edit Default Config
</Button>
<Link href={`/admin/bots/${slackBotId}/channels/new`}>
<Button variant="outline">
<FiPlusSquare />
New Channel Configuration
</Button>
</Link>
</div>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackChannelConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
<div>
<h2 className="text-2xl font- mb-4">Channel-Specific Configurations</h2>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Channel</TableHead>
<TableHead>Assistant</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channelConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackChannelConfig) => {
return (
<TableRow
key={slackChannelConfig.id}
className="cursor-pointer transition-colors"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`;
}}
>
<TableCell>
<div className="flex gap-x-2">
<div className="my-auto">
<EditIcon className="text-muted-foreground" />
</div>
<div className="my-auto">
{"#" +
slackChannelConfig.channel_config.channel_name}
</div>
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
{slackChannelConfig.persona &&
!isPersonaASlackBotPersona(
slackChannelConfig.persona
) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
className="text-primary hover:underline"
>
{slackChannelConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
<div>
{slackChannelConfig.persona &&
slackChannelConfig.persona.document_sets.length > 0
? slackChannelConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="sm"
className="hover:text-destructive"
onClick={async (e) => {
e.stopPropagation();
const response = await deleteSlackChannelConfig(
slackChannelConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackChannelConfig.id}" deleted`,
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete Slack bot config - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
>
<TrashIcon />
</Button>
</TableCell>
</TableRow>
);
})}
{channelConfigs.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No channel-specific configurations. Add a new configuration
to customize behavior for specific channels.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
{channelConfigs.length > numToDisplay && (
<div className="mt-4 flex justify-center">
<PageSelector
totalPages={Math.ceil(channelConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
)}
</div>
</div>
);

View File

@ -1,21 +1,29 @@
"use client";
import React, { useMemo } from "react";
import { Formik } from "formik";
import React, { useMemo, useState, useEffect } from "react";
import { Formik, Form, Field } from "formik";
import * as Yup from "yup";
import { usePopup } from "@/components/admin/connectors/Popup";
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
import {
DocumentSet,
SlackChannelConfig,
SlackBotResponseType,
} from "@/lib/types";
import {
createSlackChannelConfig,
isPersonaASlackBotPersona,
updateSlackChannelConfig,
fetchSlackChannels,
} from "../lib";
import CardSection from "@/components/admin/CardSection";
import { useRouter } from "next/navigation";
import { Persona } from "@/app/admin/assistants/interfaces";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { SEARCH_TOOL_ID, SEARCH_TOOL_NAME } from "@/app/chat/tools/constants";
import { SlackChannelConfigFormFields } from "./SlackChannelConfigFormFields";
import {
SlackChannelConfigFormFields,
SlackChannelConfigFormFieldsProps,
} from "./SlackChannelConfigFormFields";
export const SlackChannelConfigCreationForm = ({
slack_bot_id,
@ -33,6 +41,7 @@ export const SlackChannelConfigCreationForm = ({
const { popup, setPopup } = usePopup();
const router = useRouter();
const isUpdate = Boolean(existingSlackChannelConfig);
const isDefault = existingSlackChannelConfig?.is_default || false;
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
: false;
@ -46,13 +55,16 @@ export const SlackChannelConfigCreationForm = ({
}, [personas]);
return (
<CardSection className="max-w-4xl">
<CardSection className="!px-12 max-w-4xl">
{popup}
<Formik
initialValues={{
slack_bot_id: slack_bot_id,
channel_name:
existingSlackChannelConfig?.channel_config.channel_name || "",
channel_name: isDefault
? ""
: existingSlackChannelConfig?.channel_config.channel_name || "",
response_type: "citations" as SlackBotResponseType,
answer_validity_check_enabled: (
existingSlackChannelConfig?.channel_config?.answer_filters || []
).includes("well_answered_postfilter"),
@ -90,8 +102,6 @@ export const SlackChannelConfigCreationForm = ({
!isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
? existingSlackChannelConfig.persona.id
: null,
response_type:
existingSlackChannelConfig?.response_type || "citations",
standard_answer_categories:
existingSlackChannelConfig?.standard_answer_categories || [],
knowledge_source: existingSlackBotUsesPersona
@ -102,10 +112,12 @@ export const SlackChannelConfigCreationForm = ({
}}
validationSchema={Yup.object().shape({
slack_bot_id: Yup.number().required(),
channel_name: Yup.string().required("Channel Name is required"),
response_type: Yup.string()
channel_name: isDefault
? Yup.string()
: Yup.string().required("Channel Name is required"),
response_type: Yup.mixed<SlackBotResponseType>()
.oneOf(["quotes", "citations"])
.required("Response type is required"),
.required(),
answer_validity_check_enabled: Yup.boolean().required(),
questionmark_prefilter_enabled: Yup.boolean().required(),
respond_tag_only: Yup.boolean().required(),
@ -159,6 +171,7 @@ export const SlackChannelConfigCreationForm = ({
standard_answer_categories: values.standard_answer_categories.map(
(category: any) => category.id
),
response_type: values.response_type as SlackBotResponseType,
};
if (!cleanedValues.still_need_help_enabled) {
@ -191,13 +204,22 @@ export const SlackChannelConfigCreationForm = ({
}
}}
>
<SlackChannelConfigFormFields
isUpdate={isUpdate}
documentSets={documentSets}
searchEnabledAssistants={searchEnabledAssistants}
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
setPopup={setPopup}
/>
{({ isSubmitting, values, setFieldValue }) => (
<Form>
<div className="pb-6 w-full">
<SlackChannelConfigFormFields
{...values}
isUpdate={isUpdate}
isDefault={isDefault}
documentSets={documentSets}
searchEnabledAssistants={searchEnabledAssistants}
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
setPopup={setPopup}
slack_bot_id={slack_bot_id}
/>
</div>
</Form>
)}
</Formik>
</CardSection>
);

View File

@ -1,7 +1,13 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import { FieldArray, Form, useFormikContext, ErrorMessage } from "formik";
import {
FieldArray,
Form,
useFormikContext,
ErrorMessage,
Field,
} from "formik";
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
import {
BooleanFormField,
@ -31,9 +37,15 @@ import { TooltipProvider } from "@radix-ui/react-tooltip";
import { SourceIcon } from "@/components/SourceIcon";
import Link from "next/link";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
import { fetchSlackChannels } from "../lib";
import { Badge } from "@/components/ui/badge";
import useSWR from "swr";
import { ThreeDotsLoader } from "@/components/Loading";
interface SlackChannelConfigFormFieldsProps {
export interface SlackChannelConfigFormFieldsProps {
isUpdate: boolean;
isDefault: boolean;
documentSets: DocumentSet[];
searchEnabledAssistants: Persona[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
@ -41,19 +53,23 @@ interface SlackChannelConfigFormFieldsProps {
message: string;
type: "error" | "success" | "warning";
}) => void;
slack_bot_id: number;
}
export function SlackChannelConfigFormFields({
isUpdate,
isDefault,
documentSets,
searchEnabledAssistants,
standardAnswerCategoryResponse,
setPopup,
slack_bot_id,
}: SlackChannelConfigFormFieldsProps) {
const router = useRouter();
const { values, setFieldValue } = useFormikContext<any>();
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
const [currentSearchTerm, setCurrentSearchTerm] = useState("");
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
useState(false);
@ -152,11 +168,54 @@ export function SlackChannelConfigFormFields({
);
}, [documentSets]);
return (
<Form className="px-6 max-w-4xl">
<div className="pt-4 w-full">
<TextFormField name="channel_name" label="Slack Channel Name:" />
const { data: channelOptions, isLoading } = useSWR(
`/api/manage/admin/slack-app/bots/${slack_bot_id}/channels`,
async (url: string) => {
const channels = await fetchSlackChannels(slack_bot_id);
return channels.map((channel: any) => ({
name: channel.name,
value: channel.id,
}));
}
);
if (isLoading) {
return <ThreeDotsLoader />;
}
return (
<>
<div className="w-full">
{isDefault && (
<Badge variant="agent" className="bg-blue-100 text-blue-800">
Default Configuration
</Badge>
)}
{!isDefault && (
<>
<label
htmlFor="channel_name"
className="block font-medium text-base mb-2"
>
Select A Slack Channel:
</label>{" "}
<Field name="channel_name">
{({ field, form }: { field: any; form: any }) => (
<SearchMultiSelectDropdown
options={channelOptions || []}
onSelect={(selected) => {
form.setFieldValue("channel_name", selected.name);
setCurrentSearchTerm(selected.name);
}}
initialSearchTerm={field.value}
onSearchTermChange={(term) => {
setCurrentSearchTerm(term);
form.setFieldValue("channel_name", term);
}}
/>
)}
</Field>
</>
)}
<div className="space-y-2 mt-4">
<Label>Knowledge Source</Label>
<RadioGroup
@ -170,7 +229,7 @@ export function SlackChannelConfigFormFields({
value="all_public"
id="all_public"
label="All Public Knowledge"
sublabel="Let OnyxBot respond based on information from all public connectors "
sublabel="Let OnyxBot respond based on information from all public connectors"
/>
{selectableSets.length + unselectableSets.length > 0 && (
<RadioGroupItemField
@ -188,7 +247,6 @@ export function SlackChannelConfigFormFields({
/>
</RadioGroup>
</div>
{values.knowledge_source === "document_sets" &&
documentSets.length > 0 && (
<div className="mt-4">
@ -281,7 +339,6 @@ export function SlackChannelConfigFormFields({
/>
</div>
)}
{values.knowledge_source === "assistant" && (
<div className="mt-4">
<SubLabel>
@ -353,15 +410,15 @@ export function SlackChannelConfigFormFields({
)}
</div>
<div className="mt-2">
<div className="mt-6">
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
</div>
{showAdvancedOptions && (
<div className="mt-4">
<div className="w-64 mb-4">
<div className="mt-2 space-y-4">
<div className="w-64">
<SelectorFormField
name="response_type"
label="Answer Type"
@ -380,83 +437,79 @@ export function SlackChannelConfigFormFields({
tooltip="If set, will show a button at the bottom of the response that allows the user to continue the conversation in the Onyx Web UI"
/>
<div className="flex flex-col space-y-3 mt-2">
<BooleanFormField
name="still_need_help_enabled"
removeIndent
onChange={(checked: boolean) => {
setFieldValue("still_need_help_enabled", checked);
if (!checked) {
setFieldValue("follow_up_tags", []);
}
}}
label={'Give a "Still need help?" button'}
tooltip={`OnyxBot's response will include a button at the bottom
of the response that asks the user if they still need help.`}
/>
{values.still_need_help_enabled && (
<CollapsibleSection prompt="Configure Still Need Help Button">
<TextArrayField
name="follow_up_tags"
label="(Optional) Users / Groups to Tag"
values={values}
subtext={
<div>
The Slack users / groups we should tag if the user clicks
the &quot;Still need help?&quot; button. If no emails are
provided, we will not tag anyone and will just react with
a 🆘 emoji to the original message.
</div>
}
placeholder="User email or user group name..."
/>
</CollapsibleSection>
)}
<BooleanFormField
name="answer_validity_check_enabled"
removeIndent
label="Only respond if citations found"
tooltip="If set, will only answer questions where the model successfully produces citations"
/>
<BooleanFormField
name="questionmark_prefilter_enabled"
removeIndent
label="Only respond to questions"
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
/>
<BooleanFormField
name="respond_tag_only"
removeIndent
label="Respond to @OnyxBot Only"
tooltip="If set, OnyxBot will only respond when directly tagged"
/>
<BooleanFormField
name="respond_to_bots"
removeIndent
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<BooleanFormField
name="enable_auto_filters"
removeIndent
label="Enable LLM Autofiltering"
tooltip="If set, the LLM will generate source and time filters based on the user's query"
/>
<div className="mt-12">
<BooleanFormField
name="still_need_help_enabled"
removeIndent
onChange={(checked: boolean) => {
setFieldValue("still_need_help_enabled", checked);
if (!checked) {
setFieldValue("follow_up_tags", []);
}
}}
label={'Give a "Still need help?" button'}
tooltip={`OnyxBot's response will include a button at the bottom
of the response that asks the user if they still need help.`}
/>
{values.still_need_help_enabled && (
<CollapsibleSection prompt="Configure Still Need Help Button">
<TextArrayField
name="respond_member_group_list"
label="(Optional) Respond to Certain Users / Groups"
subtext={
"If specified, OnyxBot responses will only " +
"be visible to the members or groups in this list."
}
name="follow_up_tags"
label="(Optional) Users / Groups to Tag"
values={values}
subtext={
<div>
The Slack users / groups we should tag if the user clicks
the &quot;Still need help?&quot; button. If no emails are
provided, we will not tag anyone and will just react with a
🆘 emoji to the original message.
</div>
}
placeholder="User email or user group name..."
/>
</div>
</div>
</CollapsibleSection>
)}
<BooleanFormField
name="answer_validity_check_enabled"
removeIndent
label="Only respond if citations found"
tooltip="If set, will only answer questions where the model successfully produces citations"
/>
<BooleanFormField
name="questionmark_prefilter_enabled"
removeIndent
label="Only respond to questions"
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
/>
<BooleanFormField
name="respond_tag_only"
removeIndent
label="Respond to @OnyxBot Only"
tooltip="If set, OnyxBot will only respond when directly tagged"
/>
<BooleanFormField
name="respond_to_bots"
removeIndent
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<BooleanFormField
name="enable_auto_filters"
removeIndent
label="Enable LLM Autofiltering"
tooltip="If set, the LLM will generate source and time filters based on the user's query"
/>
<TextArrayField
name="respond_member_group_list"
label="(Optional) Respond to Certain Users / Groups"
subtext={
"If specified, OnyxBot responses will only " +
"be visible to the members or groups in this list."
}
values={values}
placeholder="User email or user group name..."
/>
<StandardAnswerCategoryDropdownField
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
@ -468,7 +521,7 @@ export function SlackChannelConfigFormFields({
</div>
)}
<div className="flex mt-2 gap-x-2 w-full justify-end flex">
<div className="flex mt-8 gap-x-2 w-full justify-end">
{shouldShowPrivacyAlert && (
<TooltipProvider>
<Tooltip>
@ -518,13 +571,11 @@ export function SlackChannelConfigFormFields({
</Tooltip>
</TooltipProvider>
)}
<Button onClick={() => {}} type="submit">
{isUpdate ? "Update" : "Create"}
</Button>
<Button type="submit">{isUpdate ? "Update" : "Create"}</Button>
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancel
</Button>
</div>
</Form>
</>
);
}

View File

@ -94,3 +94,17 @@ export const deleteSlackChannelConfig = async (id: number) => {
export function isPersonaASlackBotPersona(persona: Persona) {
return persona.name.startsWith("__slack_bot_persona__");
}
export const fetchSlackChannels = async (botId: number) => {
return fetch(`/api/manage/admin/slack-app/bots/${botId}/channels`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch Slack channels");
}
return response.json();
});
};

View File

@ -78,30 +78,6 @@ function SlackBotEditPage({
/>
<Separator />
<div className="my-8" />
<Link
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-80
"
href={`/admin/bots/${unwrappedParams["bot-id"]}/channels/new`}
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Slack Channel Configuration
</div>
</Link>
<div className="mt-8">
<SlackChannelConfigsTable
slackBotId={slackBot.id}

View File

@ -52,15 +52,19 @@ export function SearchMultiSelectDropdown({
itemComponent,
onCreate,
onDelete,
onSearchTermChange,
initialSearchTerm = "",
}: {
options: StringOrNumberOption[];
onSelect: (selected: StringOrNumberOption) => void;
itemComponent?: FC<{ option: StringOrNumberOption }>;
onCreate?: (name: string) => void;
onDelete?: (name: string) => void;
onSearchTermChange?: (term: string) => void;
initialSearchTerm?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const dropdownRef = useRef<HTMLDivElement>(null);
const handleSelect = (option: StringOrNumberOption) => {
@ -89,6 +93,10 @@ export function SearchMultiSelectDropdown({
};
}, []);
useEffect(() => {
setSearchTerm(initialSearchTerm);
}, [initialSearchTerm]);
return (
<div className="relative text-left w-full" ref={dropdownRef}>
<div>
@ -105,21 +113,21 @@ export function SearchMultiSelectDropdown({
}
}}
onFocus={() => setIsOpen(true)}
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-background border border-border rounded-md shadow-sm"
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-white text-gray-800 border border-gray-300 rounded-md shadow-sm"
/>
<button
type="button"
className="absolute top-0 right-0 text-sm h-full px-2 border-l border-border"
className="absolute top-0 right-0 text-sm h-full px-2 border-l border-gray-300"
aria-expanded={isOpen}
aria-haspopup="true"
onClick={() => setIsOpen(!isOpen)}
>
<ChevronDownIcon className="my-auto w-4 h-4" />
<ChevronDownIcon className="my-auto w-4 h-4 text-gray-600" />
</button>
</div>
{isOpen && (
<div className="absolute z-10 mt-1 w-full rounded-md shadow-lg bg-background border border-border max-h-60 overflow-y-auto">
<div className="absolute z-10 mt-1 w-full rounded-md shadow-lg bg-white border border-gray-300 max-h-60 overflow-y-auto">
<div
role="menu"
aria-orientation="vertical"
@ -152,9 +160,9 @@ export function SearchMultiSelectDropdown({
option.name.toLowerCase() === searchTerm.toLowerCase()
) && (
<>
<div className="border-t border-border"></div>
<div className="border-t border-gray-300"></div>
<button
className="w-full text-left flex items-center px-4 py-2 text-sm hover:bg-hover"
className="w-full text-left flex items-center px-4 py-2 text-sm text-gray-800 hover:bg-gray-100"
role="menuitem"
onClick={() => {
onCreate(searchTerm);
@ -162,7 +170,7 @@ export function SearchMultiSelectDropdown({
setSearchTerm("");
}}
>
<PlusIcon className="w-4 h-4 mr-2" />
<PlusIcon className="w-4 h-4 mr-2 text-gray-600" />
Create label &quot;{searchTerm}&quot;
</button>
</>
@ -170,7 +178,7 @@ export function SearchMultiSelectDropdown({
{filteredOptions.length === 0 &&
(!onCreate || searchTerm.trim() === "") && (
<div className="px-4 py-2.5 text-sm text-text-muted">
<div className="px-4 py-2.5 text-sm text-gray-500">
No matches found
</div>
)}

View File

@ -10,7 +10,9 @@ const badgeVariants = cva(
variant: {
"agent-faded":
"border-neutral-200 bg-neutral-100 text-neutral-600 hover:bg-neutral-200",
agent: "border-agent bg-agent text-white hover:bg-agent-hover",
agent:
"border-orange-200 bg-orange-50 text-orange-600 hover:bg-orange-75 dark:bg-orange-900 dark:text-neutral-50 dark:hover:bg-orange-850",
canceled:
"border-gray-200 bg-gray-50 text-gray-600 hover:bg-gray-75 dark:bg-gray-900 dark:text-neutral-50 dark:hover:bg-gray-850",
orange:

View File

@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
{
variants: {
variant: {
@ -58,8 +58,8 @@ const buttonVariants = cva(
size: {
default: "h-10 px-4 py-2",
xs: "h-8 px-3 py-1",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "h-10 w-10",
},
reverse: {
@ -118,7 +118,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
return (
<div className="relative group">
{button}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-neutral-800 text-white text-sm rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-neutral-800 text-white text-sm rounded-sm opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
{tooltip}
</div>
</div>

View File

@ -258,23 +258,34 @@ export type SlackBotResponseType = "quotes" | "citations";
export interface SlackChannelConfig {
id: number;
slack_bot_id: number;
persona_id: number | null;
persona: Persona | null;
channel_config: ChannelConfig;
response_type: SlackBotResponseType;
standard_answer_categories: StandardAnswerCategory[];
enable_auto_filters: boolean;
standard_answer_categories: StandardAnswerCategory[];
is_default: boolean;
}
export interface SlackBot {
export interface SlackChannelDescriptor {
id: string;
name: string;
}
export type SlackBot = {
id: number;
name: string;
enabled: boolean;
configs_count: number;
// tokens
slack_channel_configs: Array<{
id: number;
is_default: boolean;
channel_config: {
channel_name: string;
};
}>;
bot_token: string;
app_token: string;
}
};
export interface SlackBotTokens {
bot_token: string;