mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-22 05:50:57 +02:00
Allows for Slackbots that do not have search enabled
Allow no search
This commit is contained in:
commit
396f096dda
@ -0,0 +1,53 @@
|
|||||||
|
"""delete non-search assistants
|
||||||
|
|
||||||
|
Revision ID: f5437cc136c5
|
||||||
|
Revises: eaa3b5593925
|
||||||
|
Create Date: 2025-02-04 16:17:15.677256
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "f5437cc136c5"
|
||||||
|
down_revision = "eaa3b5593925"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Fix: split the statements into multiple op.execute() calls
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
WITH personas_without_search AS (
|
||||||
|
SELECT p.id
|
||||||
|
FROM persona p
|
||||||
|
LEFT JOIN persona__tool pt ON p.id = pt.persona_id
|
||||||
|
LEFT JOIN tool t ON pt.tool_id = t.id
|
||||||
|
GROUP BY p.id
|
||||||
|
HAVING COUNT(CASE WHEN t.in_code_tool_id = 'run_search' THEN 1 END) = 0
|
||||||
|
)
|
||||||
|
UPDATE slack_channel_config
|
||||||
|
SET persona_id = NULL
|
||||||
|
WHERE is_default = TRUE AND persona_id IN (SELECT id FROM personas_without_search)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
WITH personas_without_search AS (
|
||||||
|
SELECT p.id
|
||||||
|
FROM persona p
|
||||||
|
LEFT JOIN persona__tool pt ON p.id = pt.persona_id
|
||||||
|
LEFT JOIN tool t ON pt.tool_id = t.id
|
||||||
|
GROUP BY p.id
|
||||||
|
HAVING COUNT(CASE WHEN t.in_code_tool_id = 'run_search' THEN 1 END) = 0
|
||||||
|
)
|
||||||
|
DELETE FROM slack_channel_config
|
||||||
|
WHERE is_default = FALSE AND persona_id IN (SELECT id FROM personas_without_search)
|
||||||
|
"""
|
||||||
|
)
|
@ -11,6 +11,7 @@ from sqlalchemy import Select
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy import update
|
from sqlalchemy import update
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@ -708,3 +709,15 @@ def update_persona_label(
|
|||||||
def delete_persona_label(label_id: int, db_session: Session) -> None:
|
def delete_persona_label(label_id: int, db_session: Session) -> None:
|
||||||
db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).delete()
|
db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).delete()
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def persona_has_search_tool(persona_id: int, db_session: Session) -> bool:
|
||||||
|
persona = (
|
||||||
|
db_session.query(Persona)
|
||||||
|
.options(joinedload(Persona.tools))
|
||||||
|
.filter(Persona.id == persona_id)
|
||||||
|
.one_or_none()
|
||||||
|
)
|
||||||
|
if persona is None:
|
||||||
|
raise ValueError(f"Persona with ID {persona_id} does not exist")
|
||||||
|
return any(tool.in_code_tool_id == "run_search" for tool in persona.tools)
|
||||||
|
@ -256,7 +256,7 @@ def fetch_slack_channel_config_for_channel_or_default(
|
|||||||
db_session: Session, slack_bot_id: int, channel_name: str | None
|
db_session: Session, slack_bot_id: int, channel_name: str | None
|
||||||
) -> SlackChannelConfig | None:
|
) -> SlackChannelConfig | None:
|
||||||
# attempt to find channel-specific config first
|
# attempt to find channel-specific config first
|
||||||
if channel_name:
|
if channel_name is not None:
|
||||||
sc_config = db_session.scalar(
|
sc_config = db_session.scalar(
|
||||||
select(SlackChannelConfig).where(
|
select(SlackChannelConfig).where(
|
||||||
SlackChannelConfig.slack_bot_id == slack_bot_id,
|
SlackChannelConfig.slack_bot_id == slack_bot_id,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import timeago # type: ignore
|
import timeago # type: ignore
|
||||||
@ -338,6 +339,23 @@ def _build_citations_blocks(
|
|||||||
return citations_block
|
return citations_block
|
||||||
|
|
||||||
|
|
||||||
|
def _build_answer_blocks(
|
||||||
|
answer: ChatOnyxBotResponse, fallback_answer: str
|
||||||
|
) -> list[SectionBlock]:
|
||||||
|
if not answer.answer:
|
||||||
|
answer_blocks = [SectionBlock(text=fallback_answer)]
|
||||||
|
else:
|
||||||
|
# replaces markdown links with slack format links
|
||||||
|
formatted_answer = format_slack_message(answer.answer)
|
||||||
|
answer_processed = decode_escapes(
|
||||||
|
remove_slack_text_interactions(formatted_answer)
|
||||||
|
)
|
||||||
|
answer_blocks = [
|
||||||
|
SectionBlock(text=text) for text in _split_text(answer_processed)
|
||||||
|
]
|
||||||
|
return answer_blocks
|
||||||
|
|
||||||
|
|
||||||
def _build_qa_response_blocks(
|
def _build_qa_response_blocks(
|
||||||
answer: ChatOnyxBotResponse,
|
answer: ChatOnyxBotResponse,
|
||||||
) -> list[Block]:
|
) -> list[Block]:
|
||||||
@ -376,21 +394,10 @@ def _build_qa_response_blocks(
|
|||||||
|
|
||||||
filter_block = SectionBlock(text=f"_{filter_text}_")
|
filter_block = SectionBlock(text=f"_{filter_text}_")
|
||||||
|
|
||||||
if not answer.answer:
|
answer_blocks = _build_answer_blocks(
|
||||||
answer_blocks = [
|
answer=answer,
|
||||||
SectionBlock(
|
fallback_answer="Sorry, I was unable to find an answer, but I did find some potentially relevant docs 🤓",
|
||||||
text="Sorry, I was unable to find an answer, but I did find some potentially relevant docs 🤓"
|
|
||||||
)
|
)
|
||||||
]
|
|
||||||
else:
|
|
||||||
# replaces markdown links with slack format links
|
|
||||||
formatted_answer = format_slack_message(answer.answer)
|
|
||||||
answer_processed = decode_escapes(
|
|
||||||
remove_slack_text_interactions(formatted_answer)
|
|
||||||
)
|
|
||||||
answer_blocks = [
|
|
||||||
SectionBlock(text=text) for text in _split_text(answer_processed)
|
|
||||||
]
|
|
||||||
|
|
||||||
response_blocks: list[Block] = []
|
response_blocks: list[Block] = []
|
||||||
|
|
||||||
@ -481,6 +488,7 @@ def build_slack_response_blocks(
|
|||||||
use_citations: bool,
|
use_citations: bool,
|
||||||
feedback_reminder_id: str | None,
|
feedback_reminder_id: str | None,
|
||||||
skip_ai_feedback: bool = False,
|
skip_ai_feedback: bool = False,
|
||||||
|
expecting_search_result: bool = False,
|
||||||
) -> list[Block]:
|
) -> list[Block]:
|
||||||
"""
|
"""
|
||||||
This function is a top level function that builds all the blocks for the Slack response.
|
This function is a top level function that builds all the blocks for the Slack response.
|
||||||
@ -491,10 +499,20 @@ def build_slack_response_blocks(
|
|||||||
message_info.thread_messages[-1].message, message_info.is_bot_msg
|
message_info.thread_messages[-1].message, message_info.is_bot_msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if expecting_search_result:
|
||||||
answer_blocks = _build_qa_response_blocks(
|
answer_blocks = _build_qa_response_blocks(
|
||||||
answer=answer,
|
answer=answer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
answer_blocks = cast(
|
||||||
|
list[Block],
|
||||||
|
_build_answer_blocks(
|
||||||
|
answer=answer,
|
||||||
|
fallback_answer="Sorry, I was unable to generate an answer.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
web_follow_up_block = []
|
web_follow_up_block = []
|
||||||
if channel_conf and channel_conf.get("show_continue_in_web_ui"):
|
if channel_conf and channel_conf.get("show_continue_in_web_ui"):
|
||||||
web_follow_up_block.append(
|
web_follow_up_block.append(
|
||||||
|
@ -27,6 +27,7 @@ from onyx.db.engine import get_session_with_tenant
|
|||||||
from onyx.db.models import SlackChannelConfig
|
from onyx.db.models import SlackChannelConfig
|
||||||
from onyx.db.models import User
|
from onyx.db.models import User
|
||||||
from onyx.db.persona import get_persona_by_id
|
from onyx.db.persona import get_persona_by_id
|
||||||
|
from onyx.db.persona import persona_has_search_tool
|
||||||
from onyx.db.users import get_user_by_email
|
from onyx.db.users import get_user_by_email
|
||||||
from onyx.onyxbot.slack.blocks import build_slack_response_blocks
|
from onyx.onyxbot.slack.blocks import build_slack_response_blocks
|
||||||
from onyx.onyxbot.slack.handlers.utils import send_team_member_message
|
from onyx.onyxbot.slack.handlers.utils import send_team_member_message
|
||||||
@ -106,7 +107,8 @@ def handle_regular_answer(
|
|||||||
]
|
]
|
||||||
prompt = persona.prompts[0] if persona.prompts else None
|
prompt = persona.prompts[0] if persona.prompts else None
|
||||||
|
|
||||||
should_respond_even_with_no_docs = persona.num_chunks == 0 if persona else False
|
with get_session_with_tenant(tenant_id) as db_session:
|
||||||
|
expecting_search_result = persona_has_search_tool(persona.id, db_session)
|
||||||
|
|
||||||
# TODO: Add in support for Slack to truncate messages based on max LLM context
|
# TODO: Add in support for Slack to truncate messages based on max LLM context
|
||||||
# llm, _ = get_llms_for_persona(persona)
|
# llm, _ = get_llms_for_persona(persona)
|
||||||
@ -303,12 +305,12 @@ def handle_regular_answer(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
retrieval_info = answer.docs
|
retrieval_info = answer.docs
|
||||||
if not retrieval_info:
|
if not retrieval_info and expecting_search_result:
|
||||||
# This should not happen, even with no docs retrieved, there is still info returned
|
# This should not happen, even with no docs retrieved, there is still info returned
|
||||||
raise RuntimeError("Failed to retrieve docs, cannot answer question.")
|
raise RuntimeError("Failed to retrieve docs, cannot answer question.")
|
||||||
|
|
||||||
top_docs = retrieval_info.top_documents
|
top_docs = retrieval_info.top_documents if retrieval_info else []
|
||||||
if not top_docs and not should_respond_even_with_no_docs:
|
if not top_docs and expecting_search_result:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Unable to answer question: '{user_message}' - no documents found"
|
f"Unable to answer question: '{user_message}' - no documents found"
|
||||||
)
|
)
|
||||||
@ -337,7 +339,8 @@ def handle_regular_answer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
only_respond_if_citations
|
expecting_search_result
|
||||||
|
and only_respond_if_citations
|
||||||
and not answer.citations
|
and not answer.citations
|
||||||
and not message_info.bypass_filters
|
and not message_info.bypass_filters
|
||||||
):
|
):
|
||||||
@ -363,6 +366,7 @@ def handle_regular_answer(
|
|||||||
channel_conf=channel_conf,
|
channel_conf=channel_conf,
|
||||||
use_citations=True, # No longer supporting quotes
|
use_citations=True, # No longer supporting quotes
|
||||||
feedback_reminder_id=feedback_reminder_id,
|
feedback_reminder_id=feedback_reminder_id,
|
||||||
|
expecting_search_result=expecting_search_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -801,18 +801,6 @@ def process_message(
|
|||||||
channel_name=channel_name,
|
channel_name=channel_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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 (
|
|
||||||
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)
|
|
||||||
# always respond with the default configs
|
|
||||||
and not (details.is_bot_msg or details.bypass_filters)
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
follow_up = bool(
|
follow_up = bool(
|
||||||
slack_channel_config.channel_config
|
slack_channel_config.channel_config
|
||||||
and slack_channel_config.channel_config.get("follow_up_tags")
|
and slack_channel_config.channel_config.get("follow_up_tags")
|
||||||
|
135
web/package-lock.json
generated
135
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.1",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@phosphor-icons/react": "^2.0.8",
|
"@phosphor-icons/react": "^2.0.8",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
@ -3442,6 +3443,140 @@
|
|||||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
|
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collapsible": "1.1.2",
|
||||||
|
"@radix-ui/react-collection": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.1",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@phosphor-icons/react": "^2.0.8",
|
"@phosphor-icons/react": "^2.0.8",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
@ -49,7 +49,7 @@ export function SlackChannelConfigsTable({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FiSettings />
|
<FiSettings />
|
||||||
Edit Default Config
|
Edit Default Configuration
|
||||||
</Button>
|
</Button>
|
||||||
<Link href={`/admin/bots/${slackBotId}/channels/new`}>
|
<Link href={`/admin/bots/${slackBotId}/channels/new`}>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
|
@ -45,13 +45,26 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
|
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
|
||||||
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
|
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
|
||||||
: false;
|
: false;
|
||||||
|
const existingPersonaHasSearchTool = existingSlackChannelConfig?.persona
|
||||||
|
? existingSlackChannelConfig.persona.tools.some(
|
||||||
|
(tool) => tool.in_code_tool_id === SEARCH_TOOL_ID
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
const searchEnabledAssistants = useMemo(() => {
|
const [searchEnabledAssistants, nonSearchAssistants] = useMemo(() => {
|
||||||
return personas.filter((persona) => {
|
return personas.reduce(
|
||||||
return persona.tools.some(
|
(acc, persona) => {
|
||||||
(tool) => tool.in_code_tool_id == SEARCH_TOOL_ID
|
if (
|
||||||
|
persona.tools.some((tool) => tool.in_code_tool_id === SEARCH_TOOL_ID)
|
||||||
|
) {
|
||||||
|
acc[0].push(persona);
|
||||||
|
} else {
|
||||||
|
acc[1].push(persona);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[[], []] as [Persona[], Persona[]]
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}, [personas]);
|
}, [personas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -105,7 +118,9 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
standard_answer_categories:
|
standard_answer_categories:
|
||||||
existingSlackChannelConfig?.standard_answer_categories || [],
|
existingSlackChannelConfig?.standard_answer_categories || [],
|
||||||
knowledge_source: existingSlackBotUsesPersona
|
knowledge_source: existingSlackBotUsesPersona
|
||||||
|
? existingPersonaHasSearchTool
|
||||||
? "assistant"
|
? "assistant"
|
||||||
|
: "non_search_assistant"
|
||||||
: existingSlackChannelConfig?.persona
|
: existingSlackChannelConfig?.persona
|
||||||
? "document_sets"
|
? "document_sets"
|
||||||
: "all_public",
|
: "all_public",
|
||||||
@ -148,7 +163,12 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
}),
|
}),
|
||||||
standard_answer_categories: Yup.array(),
|
standard_answer_categories: Yup.array(),
|
||||||
knowledge_source: Yup.string()
|
knowledge_source: Yup.string()
|
||||||
.oneOf(["all_public", "document_sets", "assistant"])
|
.oneOf([
|
||||||
|
"all_public",
|
||||||
|
"document_sets",
|
||||||
|
"assistant",
|
||||||
|
"non_search_assistant",
|
||||||
|
])
|
||||||
.required(),
|
.required(),
|
||||||
})}
|
})}
|
||||||
onSubmit={async (values, formikHelpers) => {
|
onSubmit={async (values, formikHelpers) => {
|
||||||
@ -159,13 +179,16 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
slack_bot_id,
|
slack_bot_id,
|
||||||
channel_name: values.channel_name,
|
channel_name: values.channel_name,
|
||||||
respond_member_group_list: values.respond_member_group_list,
|
respond_member_group_list: values.respond_member_group_list,
|
||||||
usePersona: values.knowledge_source === "assistant",
|
usePersona:
|
||||||
|
values.knowledge_source === "assistant" ||
|
||||||
|
values.knowledge_source === "non_search_assistant",
|
||||||
document_sets:
|
document_sets:
|
||||||
values.knowledge_source === "document_sets"
|
values.knowledge_source === "document_sets"
|
||||||
? values.document_sets
|
? values.document_sets
|
||||||
: [],
|
: [],
|
||||||
persona_id:
|
persona_id:
|
||||||
values.knowledge_source === "assistant"
|
values.knowledge_source === "assistant" ||
|
||||||
|
values.knowledge_source === "non_search_assistant"
|
||||||
? values.persona_id
|
? values.persona_id
|
||||||
: null,
|
: null,
|
||||||
standard_answer_categories: values.standard_answer_categories.map(
|
standard_answer_categories: values.standard_answer_categories.map(
|
||||||
@ -204,7 +227,7 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, values, setFieldValue }) => (
|
{({ isSubmitting, values, setFieldValue, ...formikProps }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className="pb-6 w-full">
|
<div className="pb-6 w-full">
|
||||||
<SlackChannelConfigFormFields
|
<SlackChannelConfigFormFields
|
||||||
@ -213,9 +236,11 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
isDefault={isDefault}
|
isDefault={isDefault}
|
||||||
documentSets={documentSets}
|
documentSets={documentSets}
|
||||||
searchEnabledAssistants={searchEnabledAssistants}
|
searchEnabledAssistants={searchEnabledAssistants}
|
||||||
|
nonSearchAssistants={nonSearchAssistants}
|
||||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||||
setPopup={setPopup}
|
setPopup={setPopup}
|
||||||
slack_bot_id={slack_bot_id}
|
slack_bot_id={slack_bot_id}
|
||||||
|
formikProps={formikProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
} from "formik";
|
} from "formik";
|
||||||
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
|
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
BooleanFormField,
|
|
||||||
Label,
|
Label,
|
||||||
SelectorFormField,
|
SelectorFormField,
|
||||||
SubLabel,
|
SubLabel,
|
||||||
@ -42,18 +41,29 @@ import { fetchSlackChannels } from "../lib";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { ThreeDotsLoader } from "@/components/Loading";
|
import { ThreeDotsLoader } from "@/components/Loading";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
import { CheckFormField } from "@/components/ui/CheckField";
|
||||||
|
|
||||||
export interface SlackChannelConfigFormFieldsProps {
|
export interface SlackChannelConfigFormFieldsProps {
|
||||||
isUpdate: boolean;
|
isUpdate: boolean;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
documentSets: DocumentSet[];
|
documentSets: DocumentSet[];
|
||||||
searchEnabledAssistants: Persona[];
|
searchEnabledAssistants: Persona[];
|
||||||
|
nonSearchAssistants: Persona[];
|
||||||
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
||||||
setPopup: (popup: {
|
setPopup: (popup: {
|
||||||
message: string;
|
message: string;
|
||||||
type: "error" | "success" | "warning";
|
type: "error" | "success" | "warning";
|
||||||
}) => void;
|
}) => void;
|
||||||
slack_bot_id: number;
|
slack_bot_id: number;
|
||||||
|
formikProps: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SlackChannelConfigFormFields({
|
export function SlackChannelConfigFormFields({
|
||||||
@ -61,15 +71,15 @@ export function SlackChannelConfigFormFields({
|
|||||||
isDefault,
|
isDefault,
|
||||||
documentSets,
|
documentSets,
|
||||||
searchEnabledAssistants,
|
searchEnabledAssistants,
|
||||||
|
nonSearchAssistants,
|
||||||
standardAnswerCategoryResponse,
|
standardAnswerCategoryResponse,
|
||||||
setPopup,
|
setPopup,
|
||||||
slack_bot_id,
|
slack_bot_id,
|
||||||
|
formikProps,
|
||||||
}: SlackChannelConfigFormFieldsProps) {
|
}: SlackChannelConfigFormFieldsProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { values, setFieldValue } = useFormikContext<any>();
|
const { values, setFieldValue } = useFormikContext<any>();
|
||||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
||||||
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
|
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
|
||||||
const [currentSearchTerm, setCurrentSearchTerm] = useState("");
|
|
||||||
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
|
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@ -178,6 +188,7 @@ export function SlackChannelConfigFormFields({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <ThreeDotsLoader />;
|
return <ThreeDotsLoader />;
|
||||||
}
|
}
|
||||||
@ -194,7 +205,7 @@ export function SlackChannelConfigFormFields({
|
|||||||
<>
|
<>
|
||||||
<label
|
<label
|
||||||
htmlFor="channel_name"
|
htmlFor="channel_name"
|
||||||
className="block font-medium text-base mb-2"
|
className="block text-text font-medium text-base mb-2"
|
||||||
>
|
>
|
||||||
Select A Slack Channel:
|
Select A Slack Channel:
|
||||||
</label>{" "}
|
</label>{" "}
|
||||||
@ -204,11 +215,9 @@ export function SlackChannelConfigFormFields({
|
|||||||
options={channelOptions || []}
|
options={channelOptions || []}
|
||||||
onSelect={(selected) => {
|
onSelect={(selected) => {
|
||||||
form.setFieldValue("channel_name", selected.name);
|
form.setFieldValue("channel_name", selected.name);
|
||||||
setCurrentSearchTerm(selected.name);
|
|
||||||
}}
|
}}
|
||||||
initialSearchTerm={field.value}
|
initialSearchTerm={field.value}
|
||||||
onSearchTermChange={(term) => {
|
onSearchTermChange={(term) => {
|
||||||
setCurrentSearchTerm(term);
|
|
||||||
form.setFieldValue("channel_name", term);
|
form.setFieldValue("channel_name", term);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -242,9 +251,15 @@ export function SlackChannelConfigFormFields({
|
|||||||
<RadioGroupItemField
|
<RadioGroupItemField
|
||||||
value="assistant"
|
value="assistant"
|
||||||
id="assistant"
|
id="assistant"
|
||||||
label="Specific Assistant"
|
label="Search Assistant"
|
||||||
sublabel="Control both the documents and the prompt to use for answering questions"
|
sublabel="Control both the documents and the prompt to use for answering questions"
|
||||||
/>
|
/>
|
||||||
|
<RadioGroupItemField
|
||||||
|
value="non_search_assistant"
|
||||||
|
id="non_search_assistant"
|
||||||
|
label="Non-Search Assistant"
|
||||||
|
sublabel="Chat with an assistant that does not use documents"
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
{values.knowledge_source === "document_sets" &&
|
{values.knowledge_source === "document_sets" &&
|
||||||
@ -408,16 +423,59 @@ export function SlackChannelConfigFormFields({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{values.knowledge_source === "non_search_assistant" && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<SubLabel>
|
||||||
|
<>
|
||||||
|
Select the non-search assistant OnyxBot will use while answering
|
||||||
|
questions in Slack.
|
||||||
|
{syncEnabledAssistants.length > 0 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<span className="text-sm text-text-dark/80">
|
||||||
|
Note: Some of your assistants have auto-synced connectors
|
||||||
|
in their document sets. You cannot select these assistants
|
||||||
|
as they will not be able to answer questions in Slack.{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setViewSyncEnabledAssistants(
|
||||||
|
(viewSyncEnabledAssistants) =>
|
||||||
|
!viewSyncEnabledAssistants
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="text-sm text-link"
|
||||||
|
>
|
||||||
|
{viewSyncEnabledAssistants
|
||||||
|
? "Hide un-selectable "
|
||||||
|
: "View all "}
|
||||||
|
assistants
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</SubLabel>
|
||||||
|
|
||||||
<div className="mt-6">
|
<SelectorFormField
|
||||||
<AdvancedOptionsToggle
|
name="persona_id"
|
||||||
showAdvancedOptions={showAdvancedOptions}
|
options={nonSearchAssistants.map((persona) => ({
|
||||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
name: persona.name,
|
||||||
|
value: persona.id,
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showAdvancedOptions && (
|
)}
|
||||||
<div className="mt-2 space-y-4">
|
</div>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<Accordion type="multiple" className=" gap-y-2 w-full">
|
||||||
|
{values.knowledge_source !== "non_search_assistant" && (
|
||||||
|
<AccordionItem value="search-options">
|
||||||
|
<AccordionTrigger className="text-text">
|
||||||
|
Search Configuration
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="space-y-4">
|
||||||
<div className="w-64">
|
<div className="w-64">
|
||||||
<SelectorFormField
|
<SelectorFormField
|
||||||
name="response_type"
|
name="response_type"
|
||||||
@ -429,17 +487,34 @@ export function SlackChannelConfigFormFields({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<CheckFormField
|
||||||
|
name="enable_auto_filters"
|
||||||
|
label="Enable LLM Autofiltering"
|
||||||
|
tooltip="If set, the LLM will generate source and time filters based on the user's query"
|
||||||
|
/>
|
||||||
|
|
||||||
<BooleanFormField
|
<CheckFormField
|
||||||
|
name="answer_validity_check_enabled"
|
||||||
|
label="Only respond if citations found"
|
||||||
|
tooltip="If set, will only answer questions where the model successfully produces citations"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AccordionItem className="mt-4" value="general-options">
|
||||||
|
<AccordionTrigger>General Configuration</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CheckFormField
|
||||||
name="show_continue_in_web_ui"
|
name="show_continue_in_web_ui"
|
||||||
removeIndent
|
|
||||||
label="Show Continue in Web UI button"
|
label="Show Continue in Web UI button"
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BooleanFormField
|
<CheckFormField
|
||||||
name="still_need_help_enabled"
|
name="still_need_help_enabled"
|
||||||
removeIndent
|
|
||||||
onChange={(checked: boolean) => {
|
onChange={(checked: boolean) => {
|
||||||
setFieldValue("still_need_help_enabled", checked);
|
setFieldValue("still_need_help_enabled", checked);
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
@ -458,10 +533,10 @@ export function SlackChannelConfigFormFields({
|
|||||||
values={values}
|
values={values}
|
||||||
subtext={
|
subtext={
|
||||||
<div>
|
<div>
|
||||||
The Slack users / groups we should tag if the user clicks
|
The Slack users / groups we should tag if the user
|
||||||
the "Still need help?" button. If no emails are
|
clicks the "Still need help?" button. If no
|
||||||
provided, we will not tag anyone and will just react with a
|
emails are provided, we will not tag anyone and will
|
||||||
🆘 emoji to the original message.
|
just react with a 🆘 emoji to the original message.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placeholder="User email or user group name..."
|
placeholder="User email or user group name..."
|
||||||
@ -469,36 +544,21 @@ export function SlackChannelConfigFormFields({
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<BooleanFormField
|
<CheckFormField
|
||||||
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"
|
name="questionmark_prefilter_enabled"
|
||||||
removeIndent
|
|
||||||
label="Only respond to questions"
|
label="Only respond to questions"
|
||||||
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
|
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
|
||||||
/>
|
/>
|
||||||
<BooleanFormField
|
<CheckFormField
|
||||||
name="respond_tag_only"
|
name="respond_tag_only"
|
||||||
removeIndent
|
|
||||||
label="Respond to @OnyxBot Only"
|
label="Respond to @OnyxBot Only"
|
||||||
tooltip="If set, OnyxBot will only respond when directly tagged"
|
tooltip="If set, OnyxBot will only respond when directly tagged"
|
||||||
/>
|
/>
|
||||||
<BooleanFormField
|
<CheckFormField
|
||||||
name="respond_to_bots"
|
name="respond_to_bots"
|
||||||
removeIndent
|
|
||||||
label="Respond to Bot messages"
|
label="Respond to Bot messages"
|
||||||
tooltip="If not set, OnyxBot will always ignore messages from Bots"
|
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
|
<TextArrayField
|
||||||
name="respond_member_group_list"
|
name="respond_member_group_list"
|
||||||
@ -519,7 +579,9 @@ export function SlackChannelConfigFormFields({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<div className="flex mt-8 gap-x-2 w-full justify-end">
|
<div className="flex mt-8 gap-x-2 w-full justify-end">
|
||||||
{shouldShowPrivacyAlert && (
|
{shouldShowPrivacyAlert && (
|
||||||
|
@ -51,7 +51,7 @@ export function Label({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`block font-medium base ${className} ${
|
className={`block text-text-darker font-medium base ${className} ${
|
||||||
small ? "text-xs" : "text-sm"
|
small ? "text-xs" : "text-sm"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
102
web/src/components/ui/CheckField.tsx
Normal file
102
web/src/components/ui/CheckField.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { useField } from "formik";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
interface CheckFieldProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
sublabel?: string;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
tooltip?: string;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckFormField: React.FC<CheckFieldProps> = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
sublabel,
|
||||||
|
size = "md",
|
||||||
|
tooltip,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [field, , helpers] = useField<boolean>({ name, type: "checkbox" });
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: "h-2 w-2",
|
||||||
|
md: "h-3 w-3",
|
||||||
|
lg: "h-4 w-4",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLLabelElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
helpers.setValue(!field.value);
|
||||||
|
onChange?.(field.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkboxContent = (
|
||||||
|
<div className="flex w-fit items-start space-x-2">
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
id={name}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
helpers.setValue(Boolean(checked));
|
||||||
|
onChange?.(Boolean(checked));
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"peer shrink-0 rounded-sm border border-neutral-200 bg-white ring-offset-white " +
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 " +
|
||||||
|
"focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 " +
|
||||||
|
"data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 " +
|
||||||
|
"dark:border-neutral-800 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 " +
|
||||||
|
"dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
|
||||||
|
sizeClasses[size]
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
|
||||||
|
<Check className={sizeClasses[size]} />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className="flex flex-col cursor-pointer"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{sublabel && (
|
||||||
|
<span className="text-sm text-muted-foreground mt-1">
|
||||||
|
{sublabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return tooltip ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{checkboxContent}</TooltipTrigger>
|
||||||
|
<TooltipContent className="mb-4" side="top" align="center">
|
||||||
|
<p>{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
checkboxContent
|
||||||
|
);
|
||||||
|
};
|
60
web/src/components/ui/accordion.tsx
Normal file
60
web/src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AccordionItem.displayName = "AccordionItem";
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 text-base items-center text-text justify-between pb-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
));
|
||||||
|
AccordionTrigger.displayName = "AccordionTrigger";
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="pb-4 pt-0">{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
));
|
||||||
|
AccordionContent.displayName = "AccordionContent";
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
Loading…
x
Reference in New Issue
Block a user