Compare commits

...

86 Commits

Author SHA1 Message Date
pablodanswer
0857a995fb k 2025-01-15 19:54:05 -08:00
pablodanswer
4f05466fd8 unfortunate auth redirect fix 2025-01-15 19:51:05 -08:00
pablodanswer
9071d11295 various improvements 2025-01-15 19:23:09 -08:00
pablodanswer
49a26025c1 remove logs 2025-01-15 10:59:10 -08:00
pablodanswer
4593c782f6 post rebase fix 2025-01-14 18:29:53 -08:00
pablodanswer
f3404be997 better built-in indicators 2025-01-14 18:25:40 -08:00
pablodanswer
30dc3cce9f update label 2025-01-14 18:25:40 -08:00
pablodanswer
e9313287cf remove checkmark 2025-01-14 18:25:40 -08:00
Yuhong Sun
7d721748e5 Yuhong 2025-01-14 18:25:40 -08:00
pablodanswer
7453da8143 nit 2025-01-14 18:25:40 -08:00
pablodanswer
6584fba1cd validate 2025-01-14 18:25:40 -08:00
pablodanswer
bd8681b4f5 label filtering 2025-01-14 18:25:40 -08:00
pablodanswer
2e146f762b ensure all pushed 2025-01-14 18:25:40 -08:00
pablodanswer
73698da6e2 allow users to create labels 2025-01-14 18:25:40 -08:00
pablodanswer
6e26e0b528 beautiful assistant filter 2025-01-14 18:25:40 -08:00
pablodanswer
a58c32b9dd various changes 2025-01-14 18:25:40 -08:00
pablodanswer
89a44e680a validated 2025-01-14 18:25:40 -08:00
pablodanswer
3fd8da649f additional label filtering 2025-01-14 18:25:40 -08:00
pablodanswer
3571a1cc5d label editing / deletion 2025-01-14 18:25:40 -08:00
pablodanswer
5ea65031c9 fully cleaned assistant editor 2025-01-14 18:25:40 -08:00
pablodanswer
7f7e4fe19b user settings etc. 2025-01-14 18:25:40 -08:00
pablodanswer
a392f219b9 various improvements 2025-01-14 18:25:40 -08:00
pablodanswer
5d0866c990 k 2025-01-14 18:25:40 -08:00
pablodanswer
6e99805224 add shortcuts, reorganize various pages,update seeding, starter messages, etc. 2025-01-14 18:25:40 -08:00
pablodanswer
5509d1cbb6 quick v1 labels 2025-01-14 18:25:39 -08:00
pablodanswer
df9470c571 k 2025-01-14 18:25:22 -08:00
pablodanswer
1bbcb9cbb6 fix width + editors 2025-01-14 18:25:22 -08:00
pablodanswer
07c2acadad remove some whitespace 2025-01-14 18:25:22 -08:00
pablodanswer
a72d5d8600 quick nit 2025-01-14 18:25:22 -08:00
pablodanswer
a570022a73 push quick changes from diff 2025-01-14 18:25:22 -08:00
pablodanswer
d9bd26be2a push 2025-01-14 18:25:22 -08:00
Yuhong Sun
883baad035 k 2025-01-14 18:25:22 -08:00
Yuhong Sun
db0960e2ad Yuhong 2025-01-14 18:25:21 -08:00
pablodanswer
8c0773a6d4 quick nit 2025-01-14 18:25:06 -08:00
pablodanswer
375affe39e k 2025-01-14 18:25:06 -08:00
pablodanswer
8974562a5e add input prompts 2025-01-14 18:25:06 -08:00
pablodanswer
28a2a2b8fd l 2025-01-14 18:25:06 -08:00
pablodanswer
4dbbffc511 sidebar 2025-01-14 18:25:06 -08:00
pablodanswer
2946640bdc quick nit 2025-01-14 18:25:06 -08:00
Yuhong Sun
459073d4af Yuhong 2025-01-14 18:25:06 -08:00
pablodanswer
40f9e2b2d3 update assistant editor 2025-01-14 18:25:06 -08:00
pablodanswer
21343dfbeb quick updates 2025-01-14 18:25:06 -08:00
Yuhong Sun
ae425fd92e Yuhong 2025-01-14 18:25:06 -08:00
pablodanswer
28b0db978f update chat banner 2025-01-14 18:25:06 -08:00
pablodanswer
09ee866d35 editor changes 2025-01-14 18:25:06 -08:00
pablodanswer
7b5e281c00 k 2025-01-14 18:25:06 -08:00
pablodanswer
2d617467c0 nit 2025-01-14 18:25:06 -08:00
pablodanswer
6e8da44073 nit 2025-01-14 18:25:05 -08:00
pablodanswer
c256934b3b popover within a popover > modal within a modal 2025-01-14 18:24:57 -08:00
pablodanswer
a09f9d27e2 quick nit 2025-01-14 18:24:56 -08:00
Yuhong Sun
79f987376e Yuhong 2025-01-14 18:24:40 -08:00
pablodanswer
5e6e1893c0 address all but modal within modal 2025-01-14 18:24:16 -08:00
pablodanswer
a5d374d885 k 2025-01-14 18:24:16 -08:00
pablodanswer
4b88869400 most new fixes 2025-01-14 18:24:16 -08:00
pablodanswer
ded1c6972a quick nits 2025-01-14 18:24:16 -08:00
Yuhong Sun
f4d238a80e Yuhong 2025-01-14 18:24:16 -08:00
pablodanswer
76f2b1890a quick nit 2025-01-14 18:24:16 -08:00
pablodanswer
a87f09d0db final updates 2025-01-14 18:24:16 -08:00
pablodanswer
95e533e257 fully updated - groups 2025-01-14 18:24:16 -08:00
pablodanswer
9b6963b877 k 2025-01-14 18:24:16 -08:00
pablodanswer
ea393cca7e minor nit 2025-01-14 18:24:16 -08:00
pablodanswer
4d5125758a additioanl nits 2025-01-14 18:24:16 -08:00
pablodanswer
d081e7e550 quick nit 2025-01-14 18:24:16 -08:00
pablodanswer
b8303e2ce8 draggables 2025-01-14 18:24:15 -08:00
pablodanswer
f6e8900622 k 2025-01-14 18:24:15 -08:00
pablodanswer
65ecad7177 looking good 2025-01-14 18:24:15 -08:00
pablodanswer
08b646baae nit 2025-01-14 18:24:15 -08:00
pablodanswer
08ce6b0cff fix the hydra 2025-01-14 18:24:15 -08:00
pablodanswer
1dcf0e87d4 nit 2025-01-14 18:24:15 -08:00
pablodanswer
01f1981842 quick nit 2025-01-14 18:24:15 -08:00
pablodanswer
b4df187cc0 add new ux 2025-01-14 18:24:15 -08:00
pablodanswer
84e0e2773f nit 2025-01-14 18:24:08 -08:00
pablodanswer
b74070dbb2 push minor changes 2025-01-14 18:24:08 -08:00
pablodanswer
1d1ada371e nit 2025-01-14 18:24:08 -08:00
pablodanswer
7e26defdb0 minor nit 2025-01-14 18:24:08 -08:00
pablodanswer
5d572307d1 quick nits 2025-01-14 18:24:08 -08:00
pablodanswer
c9fa092a1a v3 2025-01-14 18:24:08 -08:00
pablodanswer
c81e0ff294 quick nits 2025-01-14 18:24:08 -08:00
pablodanswer
b850d0b23c validate 2025-01-14 18:24:08 -08:00
pablodanswer
322ca224b1 nit 2025-01-14 18:24:08 -08:00
pablodanswer
bf66795f57 quick addition 2025-01-14 18:24:08 -08:00
pablodanswer
9e3a2e1e91 organize components 2025-01-14 18:24:08 -08:00
pablodanswer
f68cbdf879 quick nit 2025-01-14 18:24:08 -08:00
pablodanswer
c025ebbc08 k 2025-01-14 18:24:08 -08:00
pablodanswer
1552b2694f v2 2025-01-14 18:24:08 -08:00
pablodanswer
7e8c21c807 add chrome extension
minor clean up

additional handling

post rebase fixes

nit

quick bump

finalize

minor cleanup

organizational

Revert changes in backend directory

Revert changes in deployment directory

push misc changes

improve shortcut display + general nrf page layout

minor clean up

quick nit

update chrome

k

build fix

k

update

k
2025-01-14 18:24:08 -08:00
189 changed files with 12257 additions and 4550 deletions

View File

@@ -0,0 +1,29 @@
"""add shortcut option for users
Revision ID: 027381bce97c
Revises: 6fc7886d665d
Create Date: 2025-01-14 12:14:00.814390
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "027381bce97c"
down_revision = "6fc7886d665d"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"user",
sa.Column(
"shortcut_enabled", sa.Boolean(), nullable=False, server_default="true"
),
)
def downgrade() -> None:
op.drop_column("user", "shortcut_enabled")

View File

@@ -0,0 +1,59 @@
"""add back input prompts
Revision ID: 3c6531f32351
Revises: aeda5f2df4f6
Create Date: 2025-01-13 12:49:51.705235
"""
from alembic import op
import sqlalchemy as sa
import fastapi_users_db_sqlalchemy
# revision identifiers, used by Alembic.
revision = "3c6531f32351"
down_revision = "aeda5f2df4f6"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"inputprompt",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("prompt", sa.String(), nullable=False),
sa.Column("content", sa.String(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column("is_public", sa.Boolean(), nullable=False),
sa.Column(
"user_id",
fastapi_users_db_sqlalchemy.generics.GUID(),
nullable=True,
),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"inputprompt__user",
sa.Column("input_prompt_id", sa.Integer(), nullable=False),
sa.Column(
"user_id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False
),
sa.Column("disabled", sa.Boolean(), nullable=False, default=False),
sa.ForeignKeyConstraint(
["input_prompt_id"],
["inputprompt.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("input_prompt_id", "user_id"),
)
def downgrade() -> None:
op.drop_table("inputprompt__user")
op.drop_table("inputprompt")

View File

@@ -0,0 +1,80 @@
"""make categories labels and many to many
Revision ID: 6fc7886d665d
Revises: 3c6531f32351
Create Date: 2025-01-13 18:12:18.029112
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "6fc7886d665d"
down_revision = "3c6531f32351"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Rename persona_category table to persona_label
op.rename_table("persona_category", "persona_label")
# Create the new association table
op.create_table(
"persona__persona_label",
sa.Column("persona_id", sa.Integer(), nullable=False),
sa.Column("persona_label_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["persona_id"],
["persona.id"],
),
sa.ForeignKeyConstraint(
["persona_label_id"],
["persona_label.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("persona_id", "persona_label_id"),
)
# Copy existing relationships to the new table
op.execute(
"""
INSERT INTO persona__persona_label (persona_id, persona_label_id)
SELECT id, category_id FROM persona WHERE category_id IS NOT NULL
"""
)
# Remove the old category_id column from persona table
op.drop_column("persona", "category_id")
def downgrade() -> None:
# Rename persona_label table back to persona_category
op.rename_table("persona_label", "persona_category")
# Add back the category_id column to persona table
op.add_column("persona", sa.Column("category_id", sa.Integer(), nullable=True))
op.create_foreign_key(
"persona_category_id_fkey",
"persona",
"persona_category",
["category_id"],
["id"],
)
# Copy the first label relationship back to the persona table
op.execute(
"""
UPDATE persona
SET category_id = (
SELECT persona_label_id
FROM persona__persona_label
WHERE persona__persona_label.persona_id = persona.id
LIMIT 1
)
"""
)
# Drop the association table
op.drop_table("persona__persona_label")

View File

@@ -0,0 +1,27 @@
"""add pinned assistants
Revision ID: aeda5f2df4f6
Revises: 0f7ff6d75b57
Create Date: 2025-01-09 16:04:10.770636
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "aeda5f2df4f6"
down_revision = "0f7ff6d75b57"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"user", sa.Column("pinned_assistants", postgresql.JSONB(), nullable=True)
)
op.execute('UPDATE "user" SET pinned_assistants = chosen_assistants')
def downgrade() -> None:
op.drop_column("user", "pinned_assistants")

536
backend/chatt.txt Normal file
View File

@@ -0,0 +1,536 @@
"{\"user_message_id\": 475, \"reserved_assistant_message_id\": 476}\n"
"{\"sub_question\": \"What\", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_query\": \"ony\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_question\": \" is\", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_query\": \"x\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_question\": \" On\", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_question\": \"yx\", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_query\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_question\": \"1\", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_query\": \" features\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_question\": \"?\", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 1}\n"
"{\"sub_question\": \"\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_question\": \"What\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_question\": \" is\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_query\": \" specifications\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_question\": \" On\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \"yx\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_question\": \"2\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_query\": \"ony\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \"?\", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_query\": \"x\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \"\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_query\": \"2\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \"What\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_query\": \" applications\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \" is\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_query\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \" On\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_query\": \" use\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \"yx\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_query\": \" cases\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_question\": \"3\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_question\": \"?\", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 3}\n"
"{\"sub_question\": \"\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_question\": \"What\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_question\": \" is\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_question\": \" On\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_question\": \"yx\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_query\": \"ony\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_question\": \"4\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_question\": \"?\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_query\": \"x\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_question\": \" \", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_question\": \"\", \"level\": 0, \"level_question_nr\": 4}\n"
"{\"sub_query\": \"3\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"4\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" comparison\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" differences\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \"4\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \" product\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \"3\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \" information\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \" software\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \" features\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 0}\n"
"{\"sub_query\": \" software\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \"2\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" features\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 0}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \" software\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \" features\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \"4\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" applications\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 0}\n"
"{\"sub_query\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \" features\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \" in\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" industry\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 0}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \" specifications\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \"2\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \" applications\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 1}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"3\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" applications\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" in\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" industry\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" applications\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 1}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \"4\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" comparison\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" with\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" previous\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" use\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" versions\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \" in\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \" industry\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 3, \"query_id\": 2}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 1}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \"On\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \"yx\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \"3\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \" comparison\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \"2\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \" comparison\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \" with\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \" cases\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" previous\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 0, \"query_id\": 2}\n"
"{\"sub_query\": \" with\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \" other\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \" software\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 1, \"query_id\": 2}\n"
"{\"sub_query\": \" versions\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \" \", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"sub_query\": \"\", \"level\": 0, \"level_question_nr\": 2, \"query_id\": 2}\n"
"{\"top_documents\": [], \"rephrased_query\": \"What is Onyx 4?\", \"predicted_flow\": \"question-answer\", \"predicted_search\": \"keyword\", \"applied_source_filters\": null, \"applied_time_cutoff\": null, \"recency_bias_multiplier\": 0.5}\n"
"{\"llm_selected_doc_indices\": []}\n"
"{\"final_context_docs\": []}\n"
"{\"answer_piece\": \"I\", \"level\": 0, \"level_question_nr\": 3, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" don't\", \"level\": 0, \"level_question_nr\": 3, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" know\", \"level\": 0, \"level_question_nr\": 3, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 3, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" formerly\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" known\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" D\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"answer\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" an\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" AI\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Assistant\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" that\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" connects\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" to\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" company's\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" documents\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" applications\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" personnel\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" It\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" provides\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" chat\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" interface\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" can\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" integrate\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" with\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" any\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" large\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" language\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" model\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" (\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"LL\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"M\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \")\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"top_documents\": [], \"rephrased_query\": \"What is Onyx 2?\", \"predicted_flow\": \"question-answer\", \"predicted_search\": \"keyword\", \"applied_source_filters\": null, \"applied_time_cutoff\": null, \"recency_bias_multiplier\": 0.5}\n"
"{\"llm_selected_doc_indices\": []}\n"
"{\"final_context_docs\": []}\n"
"{\"answer_piece\": \" of\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" choice\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" designed\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" to\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" be\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" modular\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" easily\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" extens\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"ible\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" allowing\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" for\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" deployment\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" on\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" various\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" platforms\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" including\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" laptops\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" on\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"-prem\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"ise\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" or\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" cloud\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" environments\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" It\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" ensures\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" that\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" user\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" data\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" chats\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" remain\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"I\", \"level\": 0, \"level_question_nr\": 1, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" under\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" don't\", \"level\": 0, \"level_question_nr\": 1, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" the\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" user's\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" know\", \"level\": 0, \"level_question_nr\": 1, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 1, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" control\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" the\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" deployment\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" owned\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" by\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" the\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" user\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" MIT\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" licensed\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" comes\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" ready\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" for\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" production\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" use\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" featuring\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" user\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" authentication\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" role\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" management\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" chat\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" persistence\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" user\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" interface\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" for\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" configuring\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" AI\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Assist\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"ants\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" their\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" prompts\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Additionally\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" serves\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" unified\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" search\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" tool\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" across\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" common\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" workplace\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" applications\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" like\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Slack\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Google\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Drive\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" Con\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"fluence\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" enabling\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" it\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" to\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" act\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" subject\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" matter\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" expert\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" for\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" teams\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" by\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" combining\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" L\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"LM\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"s\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" with\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" team\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \"-specific\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" knowledge\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" [[1]]()\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"top_documents\": [], \"rephrased_query\": \"What is Onyx 3?\", \"predicted_flow\": \"question-answer\", \"predicted_search\": \"keyword\", \"applied_source_filters\": null, \"applied_time_cutoff\": null, \"recency_bias_multiplier\": 0.5}\n"
"{\"llm_selected_doc_indices\": []}\n"
"{\"final_context_docs\": []}\n"
"{\"answer_piece\": \"I\", \"level\": 0, \"level_question_nr\": 2, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" don't\", \"level\": 0, \"level_question_nr\": 2, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \" know\", \"level\": 0, \"level_question_nr\": 2, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 2, \"answer_type\": \"agent_sub_answer\"}\n"
"{\"top_documents\": [{\"document_id\": \"https://docs.onyx.app/introduction\", \"chunk_ind\": 0, \"semantic_identifier\": \"Introduction - Onyx Documentation\", \"link\": \"https://docs.onyx.app/introduction\", \"blurb\": \"Onyx Documentation home page\\nSearch...\\nNavigation\\nWelcome to Onyx\\nIntroduction\\nWelcome to Onyx\\nIntroduction\\nOnyx Overview\\n\\nWhat is Onyx\\nOnyx (Formerly Danswer) is the AI Assistant connected to your companys docs, apps, and people. Onyx provides a Chat interface and plugs into any LLM of your choice. Onyx can be deployed anywhere and for any scale - on a laptop, on-premise, or to cloud. Since you own the deployment, your user data and chats are fully in your own control. Onyx is MIT licensed and designed to be modular and easily extensible.\", \"source_type\": \"web\", \"boost\": 0, \"hidden\": false, \"metadata\": {}, \"score\": 0.6275177643886491, \"is_relevant\": null, \"relevance_explanation\": null, \"match_highlights\": [\"\", \"such as A customer wants feature X, is this already supported? or Wheres the pull request for feature Y?\\n<hi>Onyx</hi> can also be plugged into existing tools like Slack to get answers and AI chats directly in Slack.\\n\\nDemo\\n\\nMain <hi>Features</hi> \\n- Chat UI with the ability to select documents to chat with.\\n- Create custom AI Assistants\", \"\"], \"updated_at\": null, \"primary_owners\": null, \"secondary_owners\": null, \"is_internet\": false, \"db_doc_id\": 35923}], \"rephrased_query\": \"what is onyx 1, 2, 3, 4\", \"predicted_flow\": \"question-answer\", \"predicted_search\": \"keyword\", \"applied_source_filters\": null, \"applied_time_cutoff\": null, \"recency_bias_multiplier\": 0.5}\n"
"{\"llm_selected_doc_indices\": []}\n"
"{\"final_context_docs\": [{\"document_id\": \"https://docs.onyx.app/introduction\", \"content\": \"Onyx Documentation home page\\nSearch...\\nNavigation\\nWelcome to Onyx\\nIntroduction\\nWelcome to Onyx\\nIntroduction\\nOnyx Overview\\n\\nWhat is Onyx\\nOnyx (Formerly Danswer) is the AI Assistant connected to your companys docs, apps, and people. Onyx provides a Chat interface and plugs into any LLM of your choice. Onyx can be deployed anywhere and for any scale - on a laptop, on-premise, or to cloud. Since you own the deployment, your user data and chats are fully in your own control. Onyx is MIT licensed and designed to be modular and easily extensible. The system also comes fully ready for production usage with user authentication, role management (admin/basic users), chat persistence, and a UI for configuring Personas (AI Assistants) and their Prompts.\\nOnyx also serves as a Unified Search across all common workplace tools such as Slack, Google Drive, Confluence, etc. By combining LLMs and team specific knowledge, Onyx becomes a subject matter expert for the team. Its like ChatGPT if it had access to your teams unique knowledge! It enables questions such as A customer wants feature X, is this already supported? or Wheres the pull request for feature Y?\\nOnyx can also be plugged into existing tools like Slack to get answers and AI chats directly in Slack.\\n\\nDemo\\n\\nMain Features \\n- Chat UI with the ability to select documents to chat with.\\n- Create custom AI Assistants with different prompts and backing knowledge sets.\\n- Connect Onyx with LLM of your choice (self-host for a fully airgapped solution).\\n- Document Search + AI Answers for natural language queries.\\n- Connectors to all common workplace tools like Google Drive, Confluence, Slack, etc.\\n- Slack integration to get answers and search results directly in Slack.\\n\\nUpcoming\\n- Chat/Prompt sharing with specific teammates and user groups.\\n- Multi-modal model support, chat with images, video etc.\\n- Choosing between LLMs and parameters during chat session.\\n- Tool calling and agent configurations options.\\n- Organizational understanding and ability to locate and suggest experts from your team.\\n\\nOther Noteable Benefits of Onyx\\n- User Authentication with document level access management.\\n- Best in class Hybrid Search across all sources (BM-25 + prefix aware embedding models).\\n- Admin Dashboard to configure connectors, document-sets, access, etc.\\n- Custom deep learning models + learn from user feedback.\\n- Easy deployment and ability to host Onyx anywhere of your choosing.\\nQuickstart\", \"blurb\": \"Onyx Documentation home page\\nSearch...\\nNavigation\\nWelcome to Onyx\\nIntroduction\\nWelcome to Onyx\\nIntroduction\\nOnyx Overview\\n\\nWhat is Onyx\\nOnyx (Formerly Danswer) is the AI Assistant connected to your companys docs, apps, and people. Onyx provides a Chat interface and plugs into any LLM of your choice. Onyx can be deployed anywhere and for any scale - on a laptop, on-premise, or to cloud. Since you own the deployment, your user data and chats are fully in your own control. Onyx is MIT licensed and designed to be modular and easily extensible.\", \"semantic_identifier\": \"Introduction - Onyx Documentation\", \"source_type\": \"web\", \"metadata\": {}, \"updated_at\": null, \"link\": \"https://docs.onyx.app/introduction\", \"source_links\": {\"0\": \"https://docs.onyx.app/introduction\"}, \"match_highlights\": [\"\", \"such as A customer wants feature X, is this already supported? or Wheres the pull request for feature Y?\\n<hi>Onyx</hi> can also be plugged into existing tools like Slack to get answers and AI chats directly in Slack.\\n\\nDemo\\n\\nMain <hi>Features</hi> \\n- Chat UI with the ability to select documents to chat with.\\n- Create custom AI Assistants\", \"\"]}]}\n"
"{\"answer_piece\": \"I\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" cannot\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" reliably\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" answer\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" the\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" question\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" about\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"2\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"3\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"4\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" the\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" provided\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" information\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" only\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" describes\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" which\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" an\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" AI\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" Assistant\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" formerly\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" known\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" D\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"answer\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"1\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" connects\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" to\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" company's\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" documents\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" applications\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" personnel\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" providing\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" chat\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" interface\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" integration\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" with\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" any\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" large\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" language\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" model\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" (\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"LL\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"M\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \")\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" of\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" choice\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" It\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" designed\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" to\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" be\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" modular\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" easily\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" extens\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"ible\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" can\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" be\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" deployed\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" on\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" various\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" platforms\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" while\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" ensuring\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" user\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" data\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" control\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" It\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" also\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" serves\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" unified\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" search\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" tool\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" across\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" common\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" workplace\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" applications\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" like\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" Slack\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" Google\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" Drive\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" and\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" Con\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"fluence\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" acting\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" as\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" a\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" subject\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" matter\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" expert\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" for\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" teams\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" [[1]]()\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"{{1}}\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"There\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" is\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" no\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" information\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" available\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" regarding\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" On\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"yx\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"2\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"3\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" or\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" \", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \"4\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \",\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" so\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" I\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" cannot\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" provide\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" details\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" about\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \" them\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"answer_piece\": \".\", \"level\": 0, \"level_question_nr\": 0, \"answer_type\": \"agent_level_answer\"}\n"
"{\"citations\": []}\n"
"{\"message_id\": 476, \"parent_message\": 475, \"latest_child_message\": null, \"message\": \"I cannot reliably answer the question about Onyx 2, 3, and 4, as the provided information only describes Onyx 1, which is an AI Assistant formerly known as Danswer. Onyx 1 connects to a company's documents, applications, and personnel, providing a chat interface and integration with any large language model (LLM) of choice. It is designed to be modular, easily extensible, and can be deployed on various platforms while ensuring user data control. It also serves as a unified search tool across common workplace applications like Slack, Google Drive, and Confluence, acting as a subject matter expert for teams [[1]](){{1}}There is no information available regarding Onyx 2, 3, or 4, so I cannot provide details about them.\", \"rephrased_query\": \"what is onyx 1, 2, 3, 4\", \"context_docs\": {\"top_documents\": [{\"document_id\": \"https://docs.onyx.app/introduction\", \"chunk_ind\": 0, \"semantic_identifier\": \"Introduction - Onyx Documentation\", \"link\": \"https://docs.onyx.app/introduction\", \"blurb\": \"Onyx Documentation home page\\nSearch...\\nNavigation\\nWelcome to Onyx\\nIntroduction\\nWelcome to Onyx\\nIntroduction\\nOnyx Overview\\n\\nWhat is Onyx\\nOnyx (Formerly Danswer) is the AI Assistant connected to your companys docs, apps, and people. Onyx provides a Chat interface and plugs into any LLM of your choice. Onyx can be deployed anywhere and for any scale - on a laptop, on-premise, or to cloud. Since you own the deployment, your user data and chats are fully in your own control. Onyx is MIT licensed and designed to be modular and easily extensible.\", \"source_type\": \"web\", \"boost\": 0, \"hidden\": false, \"metadata\": {}, \"score\": 0.6275177643886491, \"is_relevant\": null, \"relevance_explanation\": null, \"match_highlights\": [\"\", \"such as A customer wants feature X, is this already supported? or Wheres the pull request for feature Y?\\n<hi>Onyx</hi> can also be plugged into existing tools like Slack to get answers and AI chats directly in Slack.\\n\\nDemo\\n\\nMain <hi>Features</hi> \\n- Chat UI with the ability to select documents to chat with.\\n- Create custom AI Assistants\", \"\"], \"updated_at\": null, \"primary_owners\": null, \"secondary_owners\": null, \"is_internet\": false, \"db_doc_id\": 35923}]}, \"message_type\": \"assistant\", \"time_sent\": \"2025-01-12T05:37:18.318251+00:00\", \"overridden_model\": \"gpt-4o\", \"alternate_assistant_id\": 0, \"chat_session_id\": \"40f91916-7419-48d1-9681-5882b0869d88\", \"citations\": {}, \"sub_questions\": [], \"files\": [], \"tool_call\": null}\n"

View File

@@ -23,6 +23,7 @@ def load_no_auth_user_preferences(store: KeyValueStore) -> UserPreferences:
preferences_data = cast(
Mapping[str, Any], store.load(KV_NO_AUTH_USER_PREFERENCES_KEY)
)
print("preferences_data", preferences_data)
return UserPreferences(**preferences_data)
except KvKeyNotFoundError:
return UserPreferences(

View File

@@ -1,6 +1,6 @@
import os
INPUT_PROMPT_YAML = "./onyx/seeding/input_prompts.yaml"
PROMPTS_YAML = "./onyx/seeding/prompts.yaml"
PERSONAS_YAML = "./onyx/seeding/personas.yaml"

View File

@@ -0,0 +1,262 @@
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy import select
from sqlalchemy.orm import aliased
from sqlalchemy.orm import Session
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.constants import AuthType
from onyx.db.models import InputPrompt
from onyx.db.models import InputPrompt__User
from onyx.db.models import User
from onyx.server.features.input_prompt.models import InputPromptSnapshot
from onyx.server.manage.models import UserInfo
from onyx.utils.logger import setup_logger
logger = setup_logger()
def insert_input_prompt_if_not_exists(
user: User | None,
input_prompt_id: int | None,
prompt: str,
content: str,
active: bool,
is_public: bool,
db_session: Session,
commit: bool = True,
) -> InputPrompt:
if input_prompt_id is not None:
input_prompt = (
db_session.query(InputPrompt).filter_by(id=input_prompt_id).first()
)
else:
query = db_session.query(InputPrompt).filter(InputPrompt.prompt == prompt)
if user:
query = query.filter(InputPrompt.user_id == user.id)
else:
query = query.filter(InputPrompt.user_id.is_(None))
input_prompt = query.first()
if input_prompt is None:
input_prompt = InputPrompt(
id=input_prompt_id,
prompt=prompt,
content=content,
active=active,
is_public=is_public or user is None,
user_id=user.id if user else None,
)
db_session.add(input_prompt)
if commit:
db_session.commit()
return input_prompt
def insert_input_prompt(
prompt: str,
content: str,
is_public: bool,
user: User | None,
db_session: Session,
) -> InputPrompt:
input_prompt = InputPrompt(
prompt=prompt,
content=content,
active=True,
is_public=is_public,
user_id=user.id if user is not None else None,
)
db_session.add(input_prompt)
db_session.commit()
return input_prompt
def update_input_prompt(
user: User | None,
input_prompt_id: int,
prompt: str,
content: str,
active: bool,
db_session: Session,
) -> InputPrompt:
input_prompt = db_session.scalar(
select(InputPrompt).where(InputPrompt.id == input_prompt_id)
)
if input_prompt is None:
raise ValueError(f"No input prompt with id {input_prompt_id}")
if not validate_user_prompt_authorization(user, input_prompt):
raise HTTPException(status_code=401, detail="You don't own this prompt")
input_prompt.prompt = prompt
input_prompt.content = content
input_prompt.active = active
db_session.commit()
return input_prompt
def validate_user_prompt_authorization(
user: User | None, input_prompt: InputPrompt
) -> bool:
prompt = InputPromptSnapshot.from_model(input_prompt=input_prompt)
if prompt.user_id is not None:
if user is None:
return False
user_details = UserInfo.from_model(user)
if str(user_details.id) != str(prompt.user_id):
return False
return True
def remove_public_input_prompt(input_prompt_id: int, db_session: Session) -> None:
input_prompt = db_session.scalar(
select(InputPrompt).where(InputPrompt.id == input_prompt_id)
)
if input_prompt is None:
raise ValueError(f"No input prompt with id {input_prompt_id}")
if not input_prompt.is_public:
raise HTTPException(status_code=400, detail="This prompt is not public")
db_session.delete(input_prompt)
db_session.commit()
def remove_input_prompt(
user: User | None,
input_prompt_id: int,
db_session: Session,
delete_public: bool = False,
) -> None:
input_prompt = db_session.scalar(
select(InputPrompt).where(InputPrompt.id == input_prompt_id)
)
if input_prompt is None:
raise ValueError(f"No input prompt with id {input_prompt_id}")
if input_prompt.is_public and not delete_public:
raise HTTPException(
status_code=400, detail="Cannot delete public prompts with this method"
)
if not validate_user_prompt_authorization(user, input_prompt):
raise HTTPException(status_code=401, detail="You do not own this prompt")
db_session.delete(input_prompt)
db_session.commit()
def fetch_input_prompt_by_id(
id: int, user_id: UUID | None, db_session: Session
) -> InputPrompt:
query = select(InputPrompt).where(InputPrompt.id == id)
if user_id:
query = query.where(
(InputPrompt.user_id == user_id) | (InputPrompt.user_id is None)
)
else:
# If no user_id is provided, only fetch prompts without a user_id (aka public)
query = query.where(InputPrompt.user_id == None) # noqa
result = db_session.scalar(query)
if result is None:
raise HTTPException(422, "No input prompt found")
return result
def fetch_public_input_prompts(
db_session: Session,
) -> list[InputPrompt]:
query = select(InputPrompt).where(InputPrompt.is_public)
return list(db_session.scalars(query).all())
def fetch_input_prompts_by_user(
db_session: Session,
user_id: UUID | None,
active: bool | None = None,
include_public: bool = False,
) -> list[InputPrompt]:
"""
Returns all prompts belonging to the user or public prompts,
excluding those the user has specifically disabled.
"""
# Start with a basic query for InputPrompt
query = select(InputPrompt)
# If we have a user, left join to InputPrompt__User so we can check "disabled"
if user_id is not None:
IPU = aliased(InputPrompt__User)
query = query.join(
IPU,
(IPU.input_prompt_id == InputPrompt.id) & (IPU.user_id == user_id),
isouter=True,
)
# Exclude disabled prompts
# i.e. keep only those where (IPU.disabled is NULL or False)
query = query.where(or_(IPU.disabled.is_(None), IPU.disabled.is_(False)))
if include_public:
# user-owned or public
query = query.where(
(InputPrompt.user_id == user_id) | (InputPrompt.is_public)
)
else:
# only user-owned prompts
query = query.where(InputPrompt.user_id == user_id)
# If no user is logged in, get all prompts (public and private)
if user_id is None and AUTH_TYPE == AuthType.DISABLED:
query = query.where(True) # type: ignore
# If no user is logged in but we want to include public prompts
elif include_public:
query = query.where(InputPrompt.is_public)
if active is not None:
query = query.where(InputPrompt.active == active)
return list(db_session.scalars(query).all())
def disable_input_prompt_for_user(
input_prompt_id: int,
user_id: UUID,
db_session: Session,
) -> None:
"""
Sets (or creates) a record in InputPrompt__User with disabled=True
so that this prompt is hidden for the user.
"""
ipu = (
db_session.query(InputPrompt__User)
.filter_by(input_prompt_id=input_prompt_id, user_id=user_id)
.first()
)
if ipu is None:
# Create a new association row
ipu = InputPrompt__User(
input_prompt_id=input_prompt_id, user_id=user_id, disabled=True
)
db_session.add(ipu)
else:
# Just update the existing record
ipu.disabled = True
db_session.commit()

View File

@@ -151,6 +151,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
# if specified, controls the assistants that are shown to the user + their order
# if not specified, all assistants are shown
auto_scroll: Mapped[bool] = mapped_column(Boolean, default=True)
shortcut_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
chosen_assistants: Mapped[list[int] | None] = mapped_column(
postgresql.JSONB(), nullable=True, default=None
)
@@ -163,6 +164,9 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
recent_assistants: Mapped[list[dict]] = mapped_column(
postgresql.JSONB(), nullable=False, default=list, server_default="[]"
)
pinned_assistants: Mapped[list[int] | None] = mapped_column(
postgresql.JSONB(), nullable=True, default=None
)
oidc_expiry: Mapped[datetime.datetime] = mapped_column(
TIMESTAMPAware(timezone=True), nullable=True
@@ -184,7 +188,9 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
)
prompts: Mapped[list["Prompt"]] = relationship("Prompt", back_populates="user")
input_prompts: Mapped[list["InputPrompt"]] = relationship(
"InputPrompt", back_populates="user"
)
# Personas owned by this user
personas: Mapped[list["Persona"]] = relationship("Persona", back_populates="user")
# Custom tools created by this user
@@ -1429,8 +1435,17 @@ class StarterMessage(TypedDict):
class StarterMessageModel(BaseModel):
name: str
message: str
name: str
class Persona__PersonaLabel(Base):
__tablename__ = "persona__persona_label"
persona_id: Mapped[int] = mapped_column(ForeignKey("persona.id"), primary_key=True)
persona_label_id: Mapped[int] = mapped_column(
ForeignKey("persona_label.id", ondelete="CASCADE"), primary_key=True
)
class Persona(Base):
@@ -1455,9 +1470,7 @@ class Persona(Base):
recency_bias: Mapped[RecencyBiasSetting] = mapped_column(
Enum(RecencyBiasSetting, native_enum=False)
)
category_id: Mapped[int | None] = mapped_column(
ForeignKey("persona_category.id"), nullable=True
)
# Allows the Persona to specify a different LLM version than is controlled
# globablly via env variables. For flexibility, validity is not currently enforced
# NOTE: only is applied on the actual response generation - is not used for things like
@@ -1529,10 +1542,11 @@ class Persona(Base):
secondary="persona__user_group",
viewonly=True,
)
category: Mapped["PersonaCategory"] = relationship(
"PersonaCategory", back_populates="personas"
labels: Mapped[list["PersonaLabel"]] = relationship(
"PersonaLabel",
secondary=Persona__PersonaLabel.__table__,
back_populates="personas",
)
# Default personas loaded via yaml cannot have the same name
__table_args__ = (
Index(
@@ -1544,14 +1558,17 @@ class Persona(Base):
)
class PersonaCategory(Base):
__tablename__ = "persona_category"
class PersonaLabel(Base):
__tablename__ = "persona_label"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String, unique=True)
description: Mapped[str | None] = mapped_column(String, nullable=True)
personas: Mapped[list["Persona"]] = relationship(
"Persona", back_populates="category"
"Persona",
secondary=Persona__PersonaLabel.__table__,
back_populates="labels",
cascade="all, delete-orphan",
single_parent=True,
)
@@ -1974,6 +1991,32 @@ class UsageReport(Base):
file = relationship("PGFileStore")
class InputPrompt(Base):
__tablename__ = "inputprompt"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
prompt: Mapped[str] = mapped_column(String)
content: Mapped[str] = mapped_column(String)
active: Mapped[bool] = mapped_column(Boolean)
user: Mapped[User | None] = relationship("User", back_populates="input_prompts")
is_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
user_id: Mapped[UUID | None] = mapped_column(
ForeignKey("user.id", ondelete="CASCADE"), nullable=True
)
class InputPrompt__User(Base):
__tablename__ = "inputprompt__user"
input_prompt_id: Mapped[int] = mapped_column(
ForeignKey("inputprompt.id"), primary_key=True
)
user_id: Mapped[UUID | None] = mapped_column(
ForeignKey("inputprompt.id"), primary_key=True
)
disabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
"""
Multi-tenancy related tables
"""

View File

@@ -28,7 +28,7 @@ from onyx.db.models import DocumentSet
from onyx.db.models import Persona
from onyx.db.models import Persona__User
from onyx.db.models import Persona__UserGroup
from onyx.db.models import PersonaCategory
from onyx.db.models import PersonaLabel
from onyx.db.models import Prompt
from onyx.db.models import StarterMessage
from onyx.db.models import Tool
@@ -460,7 +460,7 @@ def upsert_persona(
search_start_date: datetime | None = None,
builtin_persona: bool = False,
is_default_persona: bool = False,
category_id: int | None = None,
label_ids: list[int] | None = None,
chunks_above: int = CONTEXT_CHUNKS_ABOVE,
chunks_below: int = CONTEXT_CHUNKS_BELOW,
) -> Persona:
@@ -506,6 +506,12 @@ def upsert_persona(
f"specified. Specified IDs were: '{prompt_ids}'"
)
labels = None
if label_ids is not None:
labels = (
db_session.query(PersonaLabel).filter(PersonaLabel.id.in_(label_ids)).all()
)
# ensure all specified tools are valid
if tools:
validate_persona_tools(tools)
@@ -547,7 +553,7 @@ def upsert_persona(
existing_persona.uploaded_image_id = uploaded_image_id
existing_persona.is_visible = is_visible
existing_persona.search_start_date = search_start_date
existing_persona.category_id = category_id
existing_persona.labels = labels or []
# Do not delete any associations manually added unless
# a new updated list is provided
if document_sets is not None:
@@ -600,7 +606,7 @@ def upsert_persona(
is_visible=is_visible,
search_start_date=search_start_date,
is_default_persona=is_default_persona,
category_id=category_id,
labels=labels or [],
)
db_session.add(new_persona)
persona = new_persona
@@ -821,37 +827,31 @@ def delete_persona_by_name(
db_session.commit()
def get_assistant_categories(db_session: Session) -> list[PersonaCategory]:
return db_session.query(PersonaCategory).all()
def get_assistant_labels(db_session: Session) -> list[PersonaLabel]:
return db_session.query(PersonaLabel).all()
def create_assistant_category(
db_session: Session, name: str, description: str
) -> PersonaCategory:
category = PersonaCategory(name=name, description=description)
db_session.add(category)
def create_assistant_label(db_session: Session, name: str) -> PersonaLabel:
label = PersonaLabel(name=name)
db_session.add(label)
db_session.commit()
return category
return label
def update_persona_category(
category_id: int,
category_description: str,
category_name: str,
def update_persona_label(
label_id: int,
label_name: str,
db_session: Session,
) -> None:
persona_category = (
db_session.query(PersonaCategory)
.filter(PersonaCategory.id == category_id)
.one_or_none()
persona_label = (
db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).one_or_none()
)
if persona_category is None:
raise ValueError(f"Persona category with ID {category_id} does not exist")
persona_category.description = category_description
persona_category.name = category_name
if persona_label is None:
raise ValueError(f"Persona label with ID {label_id} does not exist")
persona_label.name = label_name
db_session.commit()
def delete_persona_category(category_id: int, db_session: Session) -> None:
db_session.query(PersonaCategory).filter(PersonaCategory.id == category_id).delete()
def delete_persona_label(label_id: int, db_session: Session) -> None:
db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).delete()
db_session.commit()

View File

@@ -55,6 +55,12 @@ from onyx.server.documents.indexing import router as indexing_router
from onyx.server.documents.standard_oauth import router as oauth_router
from onyx.server.features.document_set.api import router as document_set_router
from onyx.server.features.folder.api import router as folder_router
from onyx.server.features.input_prompt.api import (
admin_router as admin_input_prompt_router,
)
from onyx.server.features.input_prompt.api import (
basic_router as input_prompt_router,
)
from onyx.server.features.notifications.api import router as notification_router
from onyx.server.features.persona.api import admin_router as admin_persona_router
from onyx.server.features.persona.api import basic_router as persona_router
@@ -274,6 +280,8 @@ def get_application() -> FastAPI:
include_router_with_global_prefix_prepended(application, connector_router)
include_router_with_global_prefix_prepended(application, user_router)
include_router_with_global_prefix_prepended(application, credential_router)
include_router_with_global_prefix_prepended(application, input_prompt_router)
include_router_with_global_prefix_prepended(application, admin_input_prompt_router)
include_router_with_global_prefix_prepended(application, cc_pair_router)
include_router_with_global_prefix_prepended(application, folder_router)
include_router_with_global_prefix_prepended(application, document_set_router)

View File

@@ -1,9 +1,10 @@
PERSONA_CATEGORY_GENERATION_PROMPT = """
Based on the assistant's name, description, and instructions, generate a list of {num_categories}
Based on the assistant's name, description, and instructions, generate {num_categories}
**unique and diverse** categories that represent different types of starter messages a user
might send to initiate a conversation with this chatbot assistant.
**Ensure that the categories are varied and cover a wide range of topics related to the assistant's capabilities.**
**Ensure that the categories are relevant and cover
topics related to the assistant's capabilities.**
Provide the categories as a JSON array of strings **without any code fences or additional text**.
@@ -11,27 +12,20 @@ Provide the categories as a JSON array of strings **without any code fences or a
- **Name**: {name}
- **Description**: {description}
- **Instructions**: {instructions}
""".strip()
"""
PERSONA_STARTER_MESSAGE_CREATION_PROMPT = """
Create a starter message that a **user** might send to initiate a conversation with a chatbot assistant.
**Category**: {category}
{category_prompt}
Your response should include two parts:
1. **Title**: A short, engaging title that reflects the user's intent
(e.g., 'Need Travel Advice', 'Question About Coding', 'Looking for Book Recommendations').
2. **Message**: The actual message that the user would send to the assistant.
This should be natural, engaging, and encourage a helpful response from the assistant.
**Avoid overly specific details; keep the message general and broadly applicable.**
Your response should only include the actual message that the user would send to the assistant.
This should be natural, engaging, and encourage a helpful response from the assistant.
**Avoid overly specific details; keep the message general and broadly applicable.**
For example:
- Instead of "I've just adopted a 6-month-old Labrador puppy who's pulling on the leash,"
write "I'm having trouble training my new puppy to walk nicely on a leash."
Ensure each part is clearly labeled and separated as shown above.
Do not provide any additional text or explanation and be extremely concise
**Context about the assistant:**
@@ -41,6 +35,18 @@ Do not provide any additional text or explanation and be extremely concise
""".strip()
def format_persona_starter_message_prompt(
name: str, description: str, instructions: str, category: str | None = None
) -> str:
category_prompt = f"**Category**: {category}" if category else ""
return PERSONA_STARTER_MESSAGE_CREATION_PROMPT.format(
category_prompt=category_prompt,
name=name,
description=description,
instructions=instructions,
)
if __name__ == "__main__":
print(PERSONA_CATEGORY_GENERATION_PROMPT)
print(PERSONA_STARTER_MESSAGE_CREATION_PROMPT)

View File

@@ -1,15 +1,12 @@
import json
import re
from typing import Any
from typing import cast
from typing import Dict
from typing import List
from litellm import get_supported_openai_params
from sqlalchemy.orm import Session
from onyx.configs.chat_configs import NUM_PERSONA_PROMPT_GENERATION_CHUNKS
from onyx.configs.chat_configs import NUM_PERSONA_PROMPTS
from onyx.context.search.models import IndexFilters
from onyx.context.search.models import InferenceChunk
from onyx.context.search.postprocessing.postprocessing import cleanup_chunks
@@ -22,8 +19,8 @@ from onyx.db.models import User
from onyx.document_index.document_index_utils import get_both_index_names
from onyx.document_index.factory import get_default_document_index
from onyx.llm.factory import get_default_llms
from onyx.prompts.starter_messages import format_persona_starter_message_prompt
from onyx.prompts.starter_messages import PERSONA_CATEGORY_GENERATION_PROMPT
from onyx.prompts.starter_messages import PERSONA_STARTER_MESSAGE_CREATION_PROMPT
from onyx.utils.logger import setup_logger
from onyx.utils.threadpool_concurrency import FunctionCall
from onyx.utils.threadpool_concurrency import run_functions_in_parallel
@@ -49,7 +46,7 @@ def get_random_chunks_from_doc_sets(
return cleanup_chunks(chunks)
def parse_categories(content: str) -> List[str]:
def parse_categories(content: str) -> List[str | None]:
"""
Parses the JSON array of categories from the LLM response.
"""
@@ -73,7 +70,7 @@ def generate_start_message_prompts(
name: str,
description: str,
instructions: str,
categories: List[str],
categories: List[str | None],
chunk_contents: str,
supports_structured_output: bool,
fast_llm: Any,
@@ -84,13 +81,11 @@ def generate_start_message_prompts(
functions = []
for category in categories:
# Create a prompt specific to the category
start_message_generation_prompt = (
PERSONA_STARTER_MESSAGE_CREATION_PROMPT.format(
name=name,
description=description,
instructions=instructions,
category=category,
)
start_message_generation_prompt = format_persona_starter_message_prompt(
name=name,
description=description,
instructions=instructions,
category=category,
)
if chunk_contents:
@@ -101,89 +96,21 @@ def generate_start_message_prompts(
"\n'''"
)
if supports_structured_output:
functions.append(
FunctionCall(
fast_llm.invoke,
(start_message_generation_prompt, None, None, StarterMessage),
)
)
else:
functions.append(
FunctionCall(
fast_llm.invoke,
(start_message_generation_prompt,),
)
functions.append(
FunctionCall(
fast_llm.invoke,
(start_message_generation_prompt,),
)
)
return functions
def parse_unstructured_output(output: str) -> Dict[str, str]:
"""
Parses the assistant's unstructured output into a dictionary with keys:
- 'name' (Title)
- 'message' (Message)
"""
# Debug output
logger.debug(f"LLM Output for starter message creation: {output}")
# Patterns to match
title_pattern = r"(?i)^\**Title\**\s*:\s*(.+)"
message_pattern = r"(?i)^\**Message\**\s*:\s*(.+)"
# Initialize the response dictionary
response_dict = {}
# Split the output into lines
lines = output.strip().split("\n")
# Variables to keep track of the current key being processed
current_key = None
current_value_lines = []
for line in lines:
# Check for title
title_match = re.match(title_pattern, line.strip())
if title_match:
# Save previous key-value pair if any
if current_key and current_value_lines:
response_dict[current_key] = " ".join(current_value_lines).strip()
current_value_lines = []
current_key = "name"
current_value_lines.append(title_match.group(1).strip())
continue
# Check for message
message_match = re.match(message_pattern, line.strip())
if message_match:
if current_key and current_value_lines:
response_dict[current_key] = " ".join(current_value_lines).strip()
current_value_lines = []
current_key = "message"
current_value_lines.append(message_match.group(1).strip())
continue
# If the line doesn't match a new key, append it to the current value
if current_key:
current_value_lines.append(line.strip())
# Add the last key-value pair
if current_key and current_value_lines:
response_dict[current_key] = " ".join(current_value_lines).strip()
# Validate that the necessary keys are present
if not all(k in response_dict for k in ["name", "message"]):
raise ValueError("Failed to parse the assistant's response.")
return response_dict
def generate_starter_messages(
name: str,
description: str,
instructions: str,
document_set_ids: List[int],
generation_count: int,
db_session: Session,
user: User | None,
) -> List[StarterMessage]:
@@ -201,20 +128,26 @@ def generate_starter_messages(
isinstance(params, list) and "response_format" in params
)
# Generate categories
category_generation_prompt = PERSONA_CATEGORY_GENERATION_PROMPT.format(
name=name,
description=description,
instructions=instructions,
num_categories=NUM_PERSONA_PROMPTS,
)
categories: list[str | None] = []
category_response = fast_llm.invoke(category_generation_prompt)
categories = parse_categories(cast(str, category_response.content))
if generation_count > 1:
# Generate categories
category_generation_prompt = PERSONA_CATEGORY_GENERATION_PROMPT.format(
name=name,
description=description,
instructions=instructions,
num_categories=generation_count,
)
if not categories:
logger.error("No categories were generated.")
return []
category_response = fast_llm.invoke(category_generation_prompt)
categories = parse_categories(cast(str, category_response.content))
if not categories:
logger.error("No categories were generated.")
return []
else:
categories = [None]
# Fetch example content if document sets are provided
if document_set_ids:
@@ -254,18 +187,9 @@ def generate_starter_messages(
prompts = []
for response in results.values():
try:
if supports_structured_output:
response_dict = json.loads(response.content)
else:
response_dict = parse_unstructured_output(response.content)
starter_message = StarterMessage(
name=response_dict["name"],
message=response_dict["message"],
)
prompts.append(starter_message)
except (json.JSONDecodeError, ValueError) as e:
logger.error(f"Failed to parse starter message: {e}")
continue
starter_message = StarterMessage(
message=response.content, name=response.content
)
prompts.append(starter_message)
return prompts

View File

@@ -0,0 +1,24 @@
input_prompts:
- id: -5
prompt: "Elaborate"
content: "Elaborate on the above, give me a more in depth explanation."
active: true
is_public: true
- id: -4
prompt: "Reword"
content: "Help me rewrite the following politely and concisely for professional communication:\n"
active: true
is_public: true
- id: -3
prompt: "Email"
content: "Write a professional email for me including a subject line, signature, etc. Template the parts that need editing with [ ]. The email should cover the following points:\n"
active: true
is_public: true
- id: -2
prompt: "Debug"
content: "Provide step-by-step troubleshooting instructions for the following issue:\n"
active: true
is_public: true

View File

@@ -1,11 +1,13 @@
import yaml
from sqlalchemy.orm import Session
from onyx.configs.chat_configs import INPUT_PROMPT_YAML
from onyx.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT
from onyx.configs.chat_configs import PERSONAS_YAML
from onyx.configs.chat_configs import PROMPTS_YAML
from onyx.context.search.enums import RecencyBiasSetting
from onyx.db.document_set import get_or_create_document_set_by_name
from onyx.db.input_prompt import insert_input_prompt_if_not_exists
from onyx.db.models import DocumentSet as DocumentSetDBModel
from onyx.db.models import Persona
from onyx.db.models import Prompt as PromptDBModel
@@ -39,6 +41,29 @@ def load_prompts_from_yaml(
)
def load_input_prompts_from_yaml(
db_session: Session, input_prompts_yaml: str = INPUT_PROMPT_YAML
) -> None:
with open(input_prompts_yaml, "r") as file:
data = yaml.safe_load(file)
all_input_prompts = data.get("input_prompts", [])
for input_prompt in all_input_prompts:
# If these prompts are deleted (which is a hard delete in the DB), on server startup
# they will be recreated, but the user can always just deactivate them, just a light inconvenience
insert_input_prompt_if_not_exists(
user=None,
input_prompt_id=input_prompt.get("id"),
prompt=input_prompt["prompt"],
content=input_prompt["content"],
is_public=input_prompt["is_public"],
active=input_prompt.get("active", True),
db_session=db_session,
commit=True,
)
def load_personas_from_yaml(
db_session: Session,
personas_yaml: str = PERSONAS_YAML,
@@ -113,7 +138,7 @@ def load_personas_from_yaml(
if persona.get("num_chunks") is not None
else default_chunks,
llm_relevance_filter=persona.get("llm_relevance_filter"),
starter_messages=persona.get("starter_messages"),
starter_messages=persona.get("starter_messages", []),
llm_filter_extraction=persona.get("llm_filter_extraction"),
icon_shape=persona.get("icon_shape"),
icon_color=persona.get("icon_color"),
@@ -144,6 +169,8 @@ def load_chat_yamls(
db_session: Session,
prompt_yaml: str = PROMPTS_YAML,
personas_yaml: str = PERSONAS_YAML,
input_prompts_yaml: str = INPUT_PROMPT_YAML,
) -> None:
load_prompts_from_yaml(db_session, prompt_yaml)
load_personas_from_yaml(db_session, personas_yaml)
load_input_prompts_from_yaml(db_session, input_prompts_yaml)

View File

@@ -90,6 +90,7 @@ def get_chunk_info(
min_chunk_ind=chunk_id,
max_chunk_ind=chunk_id,
)
inference_chunks = document_index.id_based_retrieval(
chunk_requests=[chunk_request],
filters=IndexFilters(access_control_list=user_acl_filters),

View File

@@ -0,0 +1,156 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.db.engine import get_session
from onyx.db.input_prompt import disable_input_prompt_for_user
from onyx.db.input_prompt import fetch_input_prompt_by_id
from onyx.db.input_prompt import fetch_input_prompts_by_user
from onyx.db.input_prompt import insert_input_prompt
from onyx.db.input_prompt import remove_input_prompt
from onyx.db.input_prompt import remove_public_input_prompt
from onyx.db.input_prompt import update_input_prompt
from onyx.db.models import InputPrompt__User
from onyx.db.models import User
from onyx.server.features.input_prompt.models import CreateInputPromptRequest
from onyx.server.features.input_prompt.models import InputPromptSnapshot
from onyx.server.features.input_prompt.models import UpdateInputPromptRequest
from onyx.utils.logger import setup_logger
logger = setup_logger()
basic_router = APIRouter(prefix="/input_prompt")
admin_router = APIRouter(prefix="/admin/input_prompt")
@basic_router.get("")
def list_input_prompts(
user: User | None = Depends(current_user),
include_public: bool = True,
db_session: Session = Depends(get_session),
) -> list[InputPromptSnapshot]:
user_prompts = fetch_input_prompts_by_user(
user_id=user.id if user is not None else None,
db_session=db_session,
include_public=include_public,
)
return [InputPromptSnapshot.from_model(prompt) for prompt in user_prompts]
@basic_router.get("/{input_prompt_id}")
def get_input_prompt(
input_prompt_id: int,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> InputPromptSnapshot:
input_prompt = fetch_input_prompt_by_id(
id=input_prompt_id,
user_id=user.id if user is not None else None,
db_session=db_session,
)
return InputPromptSnapshot.from_model(input_prompt=input_prompt)
@basic_router.post("")
def create_input_prompt(
create_input_prompt_request: CreateInputPromptRequest,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> InputPromptSnapshot:
input_prompt = insert_input_prompt(
prompt=create_input_prompt_request.prompt,
content=create_input_prompt_request.content,
is_public=False,
user=user,
db_session=db_session,
)
if user is not None:
input_prompt_user = InputPrompt__User(
input_prompt_id=input_prompt.id, user_id=user.id
)
db_session.add(input_prompt_user)
db_session.commit()
return InputPromptSnapshot.from_model(input_prompt)
@basic_router.patch("/{input_prompt_id}")
def patch_input_prompt(
input_prompt_id: int,
update_input_prompt_request: UpdateInputPromptRequest,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> InputPromptSnapshot:
try:
updated_input_prompt = update_input_prompt(
user=user,
input_prompt_id=input_prompt_id,
prompt=update_input_prompt_request.prompt,
content=update_input_prompt_request.content,
active=update_input_prompt_request.active,
db_session=db_session,
)
except ValueError as e:
error_msg = "Error occurred while updated input prompt"
logger.warn(f"{error_msg}. Stack trace: {e}")
raise HTTPException(status_code=404, detail=error_msg)
return InputPromptSnapshot.from_model(updated_input_prompt)
@basic_router.delete("/{input_prompt_id}")
def delete_input_prompt(
input_prompt_id: int,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
delete_public: bool = False,
) -> None:
try:
remove_input_prompt(
user, input_prompt_id, db_session, delete_public=delete_public
)
except ValueError as e:
error_msg = "Error occurred while deleting input prompt"
logger.warn(f"{error_msg}. Stack trace: {e}")
raise HTTPException(status_code=404, detail=error_msg)
@admin_router.delete("/{input_prompt_id}")
def delete_public_input_prompt(
input_prompt_id: int,
_: User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> None:
try:
remove_public_input_prompt(input_prompt_id, db_session)
except ValueError as e:
error_msg = "Error occurred while deleting input prompt"
logger.warn(f"{error_msg}. Stack trace: {e}")
raise HTTPException(status_code=404, detail=error_msg)
@basic_router.post("/{input_prompt_id}/hide")
def hide_input_prompt_for_user(
input_prompt_id: int,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> None:
"""
Endpoint that marks a seed (or any) prompt as disabled for the current user,
so it won't show up in their subsequent queries.
"""
if user is None:
# if auth is disabled, just delete the prompt
delete_input_prompt(input_prompt_id, user, db_session, delete_public=True)
else:
disable_input_prompt_for_user(input_prompt_id, user.id, db_session)
return None

View File

@@ -0,0 +1,47 @@
from uuid import UUID
from pydantic import BaseModel
from onyx.db.models import InputPrompt
from onyx.utils.logger import setup_logger
logger = setup_logger()
class CreateInputPromptRequest(BaseModel):
prompt: str
content: str
is_public: bool
class UpdateInputPromptRequest(BaseModel):
prompt: str
content: str
active: bool
class InputPromptResponse(BaseModel):
id: int
prompt: str
content: str
active: bool
class InputPromptSnapshot(BaseModel):
id: int
prompt: str
content: str
active: bool
user_id: UUID | None
is_public: bool
@classmethod
def from_model(cls, input_prompt: InputPrompt) -> "InputPromptSnapshot":
return InputPromptSnapshot(
id=input_prompt.id,
prompt=input_prompt.prompt,
content=input_prompt.content,
active=input_prompt.active,
user_id=input_prompt.user_id,
is_public=input_prompt.is_public,
)

View File

@@ -23,16 +23,16 @@ from onyx.db.engine import get_session
from onyx.db.models import StarterMessageModel as StarterMessage
from onyx.db.models import User
from onyx.db.notification import create_notification
from onyx.db.persona import create_assistant_category
from onyx.db.persona import create_assistant_label
from onyx.db.persona import create_update_persona
from onyx.db.persona import delete_persona_category
from onyx.db.persona import get_assistant_categories
from onyx.db.persona import delete_persona_label
from onyx.db.persona import get_assistant_labels
from onyx.db.persona import get_persona_by_id
from onyx.db.persona import get_personas_for_user
from onyx.db.persona import mark_persona_as_deleted
from onyx.db.persona import mark_persona_as_not_deleted
from onyx.db.persona import update_all_personas_display_priority
from onyx.db.persona import update_persona_category
from onyx.db.persona import update_persona_label
from onyx.db.persona import update_persona_public_status
from onyx.db.persona import update_persona_shared_users
from onyx.db.persona import update_persona_visibility
@@ -44,8 +44,8 @@ from onyx.secondary_llm_flows.starter_message_creation import (
from onyx.server.features.persona.models import CreatePersonaRequest
from onyx.server.features.persona.models import GenerateStarterMessageRequest
from onyx.server.features.persona.models import ImageGenerationToolStatus
from onyx.server.features.persona.models import PersonaCategoryCreate
from onyx.server.features.persona.models import PersonaCategoryResponse
from onyx.server.features.persona.models import PersonaLabelCreate
from onyx.server.features.persona.models import PersonaLabelResponse
from onyx.server.features.persona.models import PersonaSharedNotificationData
from onyx.server.features.persona.models import PersonaSnapshot
from onyx.server.features.persona.models import PromptTemplateResponse
@@ -214,57 +214,53 @@ def update_persona(
)
class PersonaCategoryPatchRequest(BaseModel):
category_description: str
category_name: str
class PersonaLabelPatchRequest(BaseModel):
label_name: str
@basic_router.get("/categories")
def get_categories(
@basic_router.get("/labels")
def get_labels(
db: Session = Depends(get_session),
_: User | None = Depends(current_user),
) -> list[PersonaCategoryResponse]:
) -> list[PersonaLabelResponse]:
return [
PersonaCategoryResponse.from_model(category)
for category in get_assistant_categories(db_session=db)
PersonaLabelResponse.from_model(label)
for label in get_assistant_labels(db_session=db)
]
@admin_router.post("/categories")
def create_category(
category: PersonaCategoryCreate,
@basic_router.post("/labels")
def create_label(
label: PersonaLabelCreate,
db: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> PersonaCategoryResponse:
"""Create a new assistant category"""
category_model = create_assistant_category(
name=category.name, description=category.description, db_session=db
)
return PersonaCategoryResponse.from_model(category_model)
_: User | None = Depends(current_user),
) -> PersonaLabelResponse:
"""Create a new assistant label"""
label_model = create_assistant_label(name=label.name, db_session=db)
return PersonaLabelResponse.from_model(label_model)
@admin_router.patch("/category/{category_id}")
def patch_persona_category(
category_id: int,
persona_category_patch_request: PersonaCategoryPatchRequest,
@admin_router.patch("/label/{label_id}")
def patch_persona_label(
label_id: int,
persona_label_patch_request: PersonaLabelPatchRequest,
_: User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> None:
update_persona_category(
category_id=category_id,
category_description=persona_category_patch_request.category_description,
category_name=persona_category_patch_request.category_name,
update_persona_label(
label_id=label_id,
label_name=persona_label_patch_request.label_name,
db_session=db_session,
)
@admin_router.delete("/category/{category_id}")
def delete_category(
category_id: int,
@admin_router.delete("/label/{label_id}")
def delete_label(
label_id: int,
_: User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> None:
delete_persona_category(category_id=category_id, db_session=db_session)
delete_persona_label(label_id=label_id, db_session=db_session)
class PersonaShareRequest(BaseModel):
@@ -393,16 +389,19 @@ def build_assistant_prompts(
) -> list[StarterMessage]:
try:
logger.info(
"Generating starter messages for user: %s", user.id if user else "Anonymous"
f"Generating {generate_persona_prompt_request.generation_count} starter messages"
f" for user: {user.id if user else 'Anonymous'}",
)
return generate_starter_messages(
starter_messages = generate_starter_messages(
name=generate_persona_prompt_request.name,
description=generate_persona_prompt_request.description,
instructions=generate_persona_prompt_request.instructions,
document_set_ids=generate_persona_prompt_request.document_set_ids,
generation_count=generate_persona_prompt_request.generation_count,
db_session=db_session,
user=user,
)
return starter_messages
except Exception as e:
logger.exception("Failed to generate starter messages")
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -6,7 +6,7 @@ from pydantic import Field
from onyx.context.search.enums import RecencyBiasSetting
from onyx.db.models import Persona
from onyx.db.models import PersonaCategory
from onyx.db.models import PersonaLabel
from onyx.db.models import StarterMessage
from onyx.server.features.document_set.models import DocumentSet
from onyx.server.features.prompt.models import PromptSnapshot
@@ -14,6 +14,7 @@ from onyx.server.features.tool.models import ToolSnapshot
from onyx.server.models import MinimalUserSnapshot
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -23,6 +24,7 @@ class GenerateStarterMessageRequest(BaseModel):
description: str
instructions: str
document_set_ids: list[int]
generation_count: int
class CreatePersonaRequest(BaseModel):
@@ -50,7 +52,7 @@ class CreatePersonaRequest(BaseModel):
is_default_persona: bool = False
display_priority: int | None = None
search_start_date: datetime | None = None
category_id: int | None = None
label_ids: list[int]
class PersonaSnapshot(BaseModel):
@@ -78,7 +80,7 @@ class PersonaSnapshot(BaseModel):
uploaded_image_id: str | None = None
is_default_persona: bool
search_start_date: datetime | None = None
category_id: int | None = None
labels: list["PersonaLabelSnapshot"]
@classmethod
def from_model(
@@ -126,7 +128,7 @@ class PersonaSnapshot(BaseModel):
icon_shape=persona.icon_shape,
uploaded_image_id=persona.uploaded_image_id,
search_start_date=persona.search_start_date,
category_id=persona.category_id,
labels=[PersonaLabelSnapshot.from_model(label) for label in persona.labels],
)
@@ -142,20 +144,29 @@ class ImageGenerationToolStatus(BaseModel):
is_available: bool
class PersonaCategoryCreate(BaseModel):
class PersonaLabelCreate(BaseModel):
name: str
description: str
class PersonaCategoryResponse(BaseModel):
class PersonaLabelResponse(BaseModel):
id: int
name: str
description: str | None
@classmethod
def from_model(cls, category: PersonaCategory) -> "PersonaCategoryResponse":
return PersonaCategoryResponse(
def from_model(cls, category: PersonaLabel) -> "PersonaLabelResponse":
return PersonaLabelResponse(
id=category.id,
name=category.name,
description=category.description,
)
class PersonaLabelSnapshot(BaseModel):
id: int
name: str
@classmethod
def from_model(cls, label: PersonaLabel) -> "PersonaLabelSnapshot":
return PersonaLabelSnapshot(
id=label.id,
name=label.name,
)

View File

@@ -47,6 +47,8 @@ class UserPreferences(BaseModel):
recent_assistants: list[int] | None = None
default_model: str | None = None
auto_scroll: bool | None = None
pinned_assistants: list[int] | None = None
shortcut_enabled: bool | None = None
class UserInfo(BaseModel):
@@ -83,10 +85,12 @@ class UserInfo(BaseModel):
role=user.role,
preferences=(
UserPreferences(
shortcut_enabled=user.shortcut_enabled,
auto_scroll=user.auto_scroll,
chosen_assistants=user.chosen_assistants,
default_model=user.default_model,
hidden_assistants=user.hidden_assistants,
pinned_assistants=user.pinned_assistants,
visible_assistants=user.visible_assistants,
)
),

View File

@@ -625,6 +625,30 @@ def update_user_recent_assistants(
db_session.commit()
@router.patch("/shortcut-enabled")
def update_user_shortcut_enabled(
shortcut_enabled: bool,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> None:
if user is None:
if AUTH_TYPE == AuthType.DISABLED:
store = get_kv_store()
no_auth_user = fetch_no_auth_user(store)
no_auth_user.preferences.shortcut_enabled = shortcut_enabled
set_no_auth_user_preferences(store, no_auth_user.preferences)
return
else:
raise RuntimeError("This should never happen")
db_session.execute(
update(User)
.where(User.id == user.id) # type: ignore
.values(shortcut_enabled=shortcut_enabled)
)
db_session.commit()
@router.patch("/auto-scroll")
def update_user_auto_scroll(
request: AutoScrollRequest,
@@ -673,6 +697,37 @@ def update_user_default_model(
db_session.commit()
class ReorderPinnedAssistantsRequest(BaseModel):
ordered_assistant_ids: list[int]
@router.patch("/user/pinned-assistants")
def update_user_pinned_assistants(
request: ReorderPinnedAssistantsRequest,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> None:
ordered_assistant_ids = request.ordered_assistant_ids
if user is None:
if AUTH_TYPE == AuthType.DISABLED:
store = get_kv_store()
no_auth_user = fetch_no_auth_user(store)
no_auth_user.preferences.pinned_assistants = ordered_assistant_ids
print("ordered_assistant_ids", ordered_assistant_ids)
set_no_auth_user_preferences(store, no_auth_user.preferences)
return
else:
raise RuntimeError("This should never happen")
db_session.execute(
update(User)
.where(User.id == user.id) # type: ignore
.values(pinned_assistants=ordered_assistant_ids)
)
db_session.commit()
class ChosenAssistantsRequest(BaseModel):
chosen_assistants: list[int]

View File

@@ -81,7 +81,7 @@ class ImageShape(str, Enum):
class ImageGenerationTool(Tool):
_NAME = "run_image_generation"
_DESCRIPTION = "Generate an image from a prompt."
_DISPLAY_NAME = "Image Generation Tool"
_DISPLAY_NAME = "Image Generation"
def __init__(
self,

View File

@@ -108,7 +108,7 @@ def internet_search_response_to_search_docs(
class InternetSearchTool(Tool):
_NAME = "run_internet_search"
_DISPLAY_NAME = "[Beta] Internet Search Tool"
_DISPLAY_NAME = "Internet Search"
_DESCRIPTION = "Perform an internet search for up-to-date information."
def __init__(

View File

@@ -7,7 +7,7 @@ from onyx.server.features.persona.models import PersonaSnapshot
from tests.integration.common_utils.constants import API_SERVER_URL
from tests.integration.common_utils.constants import GENERAL_HEADERS
from tests.integration.common_utils.test_models import DATestPersona
from tests.integration.common_utils.test_models import DATestPersonaCategory
from tests.integration.common_utils.test_models import DATestPersonaLabel
from tests.integration.common_utils.test_models import DATestUser
@@ -216,17 +216,16 @@ class PersonaManager:
return response.ok
class PersonaCategoryManager:
class PersonaLabelManager:
@staticmethod
def create(
category: DATestPersonaCategory,
label: DATestPersonaLabel,
user_performing_action: DATestUser | None = None,
) -> DATestPersonaCategory:
) -> DATestPersonaLabel:
response = requests.post(
f"{API_SERVER_URL}/admin/persona/categories",
f"{API_SERVER_URL}/persona/labels",
json={
"name": category.name,
"description": category.description,
"name": label.name,
},
headers=user_performing_action.headers
if user_performing_action
@@ -234,47 +233,46 @@ class PersonaCategoryManager:
)
response.raise_for_status()
response_data = response.json()
category.id = response_data["id"]
return category
label.id = response_data["id"]
return label
@staticmethod
def get_all(
user_performing_action: DATestUser | None = None,
) -> list[DATestPersonaCategory]:
) -> list[DATestPersonaLabel]:
response = requests.get(
f"{API_SERVER_URL}/persona/categories",
f"{API_SERVER_URL}/persona/labels",
headers=user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS,
)
response.raise_for_status()
return [DATestPersonaCategory(**category) for category in response.json()]
return [DATestPersonaLabel(**label) for label in response.json()]
@staticmethod
def update(
category: DATestPersonaCategory,
label: DATestPersonaLabel,
user_performing_action: DATestUser | None = None,
) -> DATestPersonaCategory:
) -> DATestPersonaLabel:
response = requests.patch(
f"{API_SERVER_URL}/admin/persona/category/{category.id}",
f"{API_SERVER_URL}/admin/persona/label/{label.id}",
json={
"category_name": category.name,
"category_description": category.description,
"label_name": label.name,
},
headers=user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS,
)
response.raise_for_status()
return category
return label
@staticmethod
def delete(
category: DATestPersonaCategory,
label: DATestPersonaLabel,
user_performing_action: DATestUser | None = None,
) -> bool:
response = requests.delete(
f"{API_SERVER_URL}/admin/persona/category/{category.id}",
f"{API_SERVER_URL}/admin/persona/label/{label.id}",
headers=user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS,
@@ -283,14 +281,11 @@ class PersonaCategoryManager:
@staticmethod
def verify(
category: DATestPersonaCategory,
label: DATestPersonaLabel,
user_performing_action: DATestUser | None = None,
) -> bool:
all_categories = PersonaCategoryManager.get_all(user_performing_action)
for fetched_category in all_categories:
if fetched_category.id == category.id:
return (
fetched_category.name == category.name
and fetched_category.description == category.description
)
all_labels = PersonaLabelManager.get_all(user_performing_action)
for fetched_label in all_labels:
if fetched_label.id == label.id:
return fetched_label.name == label.name
return False

View File

@@ -41,10 +41,9 @@ class DATestUser(BaseModel):
is_active: bool
class DATestPersonaCategory(BaseModel):
class DATestPersonaLabel(BaseModel):
id: int | None = None
name: str
description: str | None
class DATestCredential(BaseModel):

View File

@@ -4,22 +4,22 @@ import pytest
from requests.exceptions import HTTPError
from tests.integration.common_utils.managers.persona import (
PersonaCategoryManager,
PersonaLabelManager,
)
from tests.integration.common_utils.managers.user import UserManager
from tests.integration.common_utils.test_models import DATestPersonaCategory
from tests.integration.common_utils.test_models import DATestPersonaLabel
from tests.integration.common_utils.test_models import DATestUser
def test_persona_category_management(reset: None) -> None:
admin_user: DATestUser = UserManager.create(name="admin_user")
persona_category = DATestPersonaCategory(
persona_category = DATestPersonaLabel(
id=None,
name=f"Test Category {uuid4()}",
description="A description for test category",
)
persona_category = PersonaCategoryManager.create(
persona_category = PersonaLabelManager.create(
category=persona_category,
user_performing_action=admin_user,
)
@@ -27,32 +27,32 @@ def test_persona_category_management(reset: None) -> None:
f"Created persona category {persona_category.name} with id {persona_category.id}"
)
assert PersonaCategoryManager.verify(
assert PersonaLabelManager.verify(
category=persona_category,
user_performing_action=admin_user,
), "Persona category was not found after creation"
regular_user: DATestUser = UserManager.create(name="regular_user")
updated_persona_category = DATestPersonaCategory(
updated_persona_category = DATestPersonaLabel(
id=persona_category.id,
name=f"Updated {persona_category.name}",
description="An updated description",
)
with pytest.raises(HTTPError) as exc_info:
PersonaCategoryManager.update(
PersonaLabelManager.update(
category=updated_persona_category,
user_performing_action=regular_user,
)
assert exc_info.value.response is not None
assert exc_info.value.response.status_code == 403
assert PersonaCategoryManager.verify(
assert PersonaLabelManager.verify(
category=persona_category,
user_performing_action=admin_user,
), "Persona category should not have been updated by non-admin user"
result = PersonaCategoryManager.delete(
result = PersonaLabelManager.delete(
category=persona_category,
user_performing_action=regular_user,
)
@@ -60,25 +60,25 @@ def test_persona_category_management(reset: None) -> None:
result is False
), "Regular user should not be able to delete the persona category"
assert PersonaCategoryManager.verify(
assert PersonaLabelManager.verify(
category=persona_category,
user_performing_action=admin_user,
), "Persona category should not have been deleted by non-admin user"
updated_persona_category.name = f"Updated {persona_category.name}"
updated_persona_category.description = "An updated description"
updated_persona_category = PersonaCategoryManager.update(
updated_persona_category = PersonaLabelManager.update(
category=updated_persona_category,
user_performing_action=admin_user,
)
print(f"Updated persona category to {updated_persona_category.name}")
assert PersonaCategoryManager.verify(
assert PersonaLabelManager.verify(
category=updated_persona_category,
user_performing_action=admin_user,
), "Persona category was not updated by admin"
success = PersonaCategoryManager.delete(
success = PersonaLabelManager.delete(
category=persona_category,
user_performing_action=admin_user,
)
@@ -87,7 +87,7 @@ def test_persona_category_management(reset: None) -> None:
f"Deleted persona category {persona_category.name} with id {persona_category.id}"
)
assert not PersonaCategoryManager.verify(
assert not PersonaLabelManager.verify(
category=persona_category,
user_performing_action=admin_user,
), "Persona category should not exist after deletion by admin"

6
node_modules/.package-lock.json generated vendored
View File

@@ -1,6 +0,0 @@
{
"name": "danswer",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

183
package-lock.json generated Normal file
View File

@@ -0,0 +1,183 @@
{
"name": "onyx",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"react-datepicker": "^7.6.0"
},
"devDependencies": {
"@types/react-datepicker": "^6.2.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
"integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.9",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.4.tgz",
"integrity": "sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-datepicker": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz",
"integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.2",
"@types/react": "*",
"date-fns": "^3.3.1"
}
},
"node_modules/@types/react-datepicker/node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-datepicker": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.6.0.tgz",
"integrity": "sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"peer": true
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
}
}
}

8
package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"react-datepicker": "^7.6.0"
},
"devDependencies": {
"@types/react-datepicker": "^6.2.0"
}
}

View File

@@ -13,7 +13,6 @@ const cspHeader = `
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
${
process.env.NEXT_PUBLIC_CLOUD_ENABLED === "true"
? "upgrade-insecure-requests;"
@@ -27,6 +26,17 @@ const nextConfig = {
publicRuntimeConfig: {
version,
},
images: {
// Used to fetch favicons
remotePatterns: [
{
protocol: "https",
hostname: "www.google.com",
port: "",
pathname: "/s2/favicons/**",
},
],
},
async headers() {
return [
{
@@ -44,17 +54,12 @@ const nextConfig = {
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Permissions-Policy",
// Deny all permissions by default
value:
"accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()",
},

848
web/package-lock.json generated
View File

@@ -17,7 +17,10 @@
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
@@ -50,6 +53,7 @@
"posthog-js": "^1.176.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-datepicker": "^7.6.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
@@ -77,6 +81,7 @@
"devDependencies": {
"@chromatic-com/playwright": "^0.10.0",
"@tailwindcss/typography": "^0.5.10",
"@types/chrome": "^0.0.287",
"chromatic": "^11.18.1",
"eslint": "^8.48.0",
"eslint-config-next": "^14.1.0",
@@ -1194,6 +1199,21 @@
"@floating-ui/utils": "^0.2.0"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
"integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.9",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
@@ -1207,9 +1227,10 @@
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.0",
@@ -2856,6 +2877,112 @@
}
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz",
"integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-menu": "2.1.4",
"@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-dropdown-menu/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-dropdown-menu/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-dropdown-menu/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-dropdown-menu/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-dropdown-menu/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-focus-guards": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
@@ -2912,6 +3039,439 @@
}
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.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-label/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-label/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-label/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-menu": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz",
"integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@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-dismissable-layer": "1.1.3",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.1",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-roving-focus": "1.1.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "^2.6.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-menu/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-menu/node_modules/@radix-ui/react-arrow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.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-menu/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-menu/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-menu/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-menu/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "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-menu/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
"integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "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-menu/node_modules/@radix-ui/react-popper": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.1",
"@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-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "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-menu/node_modules/@radix-ui/react-portal": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-layout-effect": "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-menu/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "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-menu/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-menu/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@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-callback-ref": "1.1.0",
"@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-menu/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-menu/node_modules/react-remove-scroll": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
@@ -3063,6 +3623,196 @@
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz",
"integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "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-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-roving-focus": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "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-radio-group/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-radio-group/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-radio-group/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-radio-group/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-radio-group/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "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-radio-group/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-radio-group/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@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-callback-ref": "1.1.0",
"@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-radio-group/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-roving-focus": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
@@ -4655,6 +5405,17 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/chrome": {
"version": "0.0.287",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.287.tgz",
"integrity": "sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/filesystem": "*",
"@types/har-format": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.36",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
@@ -4738,6 +5499,30 @@
"@types/estree": "*"
}
},
"node_modules/@types/filesystem": {
"version": "0.0.36",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/filewriter": "*"
}
},
"node_modules/@types/filewriter": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/har-format": {
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
"integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -8655,15 +9440,6 @@
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -14008,6 +14784,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-datepicker": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.6.0.tgz",
"integrity": "sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/react-day-picker": {
"version": "8.10.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
@@ -14197,20 +14988,20 @@
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.1",
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -14298,21 +15089,20 @@
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"invariant": "^2.2.4",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -16052,9 +16842,9 @@
"license": "MIT"
},
"node_modules/use-callback-ref": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
@@ -16063,8 +16853,8 @@
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {

View File

@@ -19,7 +19,10 @@
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
@@ -52,6 +55,7 @@
"posthog-js": "^1.176.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-datepicker": "^7.6.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
@@ -79,6 +83,7 @@
"devDependencies": {
"@chromatic-com/playwright": "^0.10.0",
"@tailwindcss/typography": "^0.5.10",
"@types/chrome": "^0.0.287",
"chromatic": "^11.18.1",
"eslint": "^8.48.0",
"eslint-config-next": "^14.1.0",

5
web/public/logo.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M27.9998 0L10.8691 7.76944L27.9998 15.5389L45.1305 7.76944L27.9998 0ZM27.9998 40.4611L10.8691 48.2306L27.9998 56L45.1305 48.2306L27.9998 40.4611ZM48.2309 10.8691L56.0001 28.0003L48.2309 45.1314L40.4617 28.0003L48.2309 10.8691ZM15.5385 28.0001L7.76923 10.869L0 28.0001L7.76923 45.1313L15.5385 28.0001Z"
fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 466 B

7
web/public/web.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14">
<g stroke="#3B82F6" strokeLinecap="round" strokeLinejoin="round">
<circle fill="transparent" cx="7" cy="7" r="6.5" />
<path fill="transparent"
d="M.5 7h13m-4 0A11.22 11.22 0 0 1 7 13.5A11.22 11.22 0 0 1 4.5 7A11.22 11.22 0 0 1 7 .5A11.22 11.22 0 0 1 9.5 7Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -113,7 +113,7 @@ export default function Page() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleKeyPress}
className="ml-1 w-96 h-9 flex-none rounded-md border border-border bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="ml-1 w-96 h-9 flex-none rounded-md border border-border bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
{Object.entries(categorizedSources)

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
<div
className={`
cursor-pointer
${isCollapsed ? "h-6" : "pl-4 border-l-2 border-border"}
${isCollapsed ? "h-6" : "pl-6 border-l-2 border-border"}
`}
onClick={toggleCollapse}
>

View File

@@ -9,31 +9,30 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { PersonaCategory } from "./interfaces";
import { PersonaLabel } from "./interfaces";
import { PopupSpec } from "@/components/admin/connectors/Popup";
interface CategoryCardProps {
category: PersonaCategory;
onUpdate: (id: number, name: string, description: string) => void;
interface LabelCardProps {
label: PersonaLabel;
onUpdate: (id: number, name: string) => void;
onDelete: (id: number) => void;
refreshCategories: () => Promise<void>;
refreshLabels: () => Promise<void>;
setPopup: (popup: PopupSpec) => void;
}
export function CategoryCard({
category,
export function LabelCard({
label,
onUpdate,
onDelete,
refreshCategories,
}: CategoryCardProps) {
refreshLabels,
}: LabelCardProps) {
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(category.name);
const [description, setDescription] = useState(category.description);
const [name, setName] = useState(label.name);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await onUpdate(category.id, name, description);
await refreshCategories();
await onUpdate(label.id, name);
await refreshLabels();
setIsEditing(false);
};
const handleEdit = (e: React.MouseEvent) => {
@@ -42,7 +41,7 @@ export function CategoryCard({
};
return (
<Card key={category.id} className="w-full max-w-sm">
<Card key={label.id} className="w-full max-w-sm">
<CardHeader className="w-full">
<CardTitle className="text-2xl font-bold">
{isEditing ? (
@@ -52,21 +51,10 @@ export function CategoryCard({
className="text-lg font-semibold"
/>
) : (
<span>{category.name}</span>
<span>{label.name}</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="w-full">
{isEditing ? (
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="resize-none w-full"
/>
) : (
<p className="text-sm text-gray-600">{category.description}</p>
)}
</CardContent>
<CardFooter className="flex justify-end space-x-2">
{isEditing ? (
<>
@@ -91,8 +79,8 @@ export function CategoryCard({
variant="destructive"
onClick={async (e) => {
e.preventDefault();
await onDelete(category.id);
await refreshCategories();
await onDelete(label.id);
await refreshLabels();
}}
>
Delete

View File

@@ -1,198 +1,150 @@
"use client";
import { ArrayHelpers, ErrorMessage, Field, useFormikContext } from "formik";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useEffect } from "react";
import { FiInfo, FiTrash2, FiPlus } from "react-icons/fi";
} from "@/components/ui/tooltip";
import { useEffect, useState } from "react";
import { FiTrash2, FiRefreshCcw, FiRefreshCw } from "react-icons/fi";
import { StarterMessage } from "./interfaces";
import { Label } from "@/components/admin/connectors/Field";
import { Button } from "@/components/ui/button";
import { SwapIcon } from "@/components/icons/icons";
import { TextFormField } from "@/components/admin/connectors/Field";
export default function StarterMessagesList({
values,
arrayHelpers,
isRefreshing,
touchStarterMessages,
debouncedRefreshPrompts,
autoStarterMessageEnabled,
errors,
setFieldValue,
}: {
values: StarterMessage[];
arrayHelpers: ArrayHelpers;
isRefreshing: boolean;
touchStarterMessages: () => void;
debouncedRefreshPrompts: () => void;
autoStarterMessageEnabled: boolean;
errors: any;
setFieldValue: any;
}) {
const { handleChange } = useFormikContext();
const [tooltipOpen, setTooltipOpen] = useState(false);
// Group starter messages into rows of 2 for display purposes
const rows = values.reduce((acc: StarterMessage[][], curr, i) => {
if (i % 2 === 0) acc.push([curr]);
else acc[acc.length - 1].push(curr);
return acc;
}, []);
const handleInputChange = (index: number, value: string) => {
touchStarterMessages();
setFieldValue(`starter_messages.${index}.message`, value);
const canAddMore = values.length <= 6;
if (value && index === values.length - 1 && values.length < 4) {
arrayHelpers.push({ message: "" });
} else if (
!value &&
index === values.length - 2 &&
!values[values.length - 1].message
) {
arrayHelpers.pop();
}
};
return (
<div className="mt-4 flex flex-col gap-6">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="flex items-start gap-4">
<div className="grid grid-cols-2 gap-6 w-full xl:w-fit">
{row.map((starterMessage, colIndex) => (
<div
key={rowIndex * 2 + colIndex}
className="bg-white max-w-full w-full xl:w-[500px] border border-border rounded-lg shadow-md transition-shadow duration-200 p-6"
>
<div className="space-y-5">
{isRefreshing ? (
<div className="w-full">
<div className="w-full">
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-10 w-full bg-gray-200 rounded animate-pulse" />
</div>
<div>
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-10 w-full bg-gray-200 rounded animate-pulse" />
</div>
<div>
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-24 w-full bg-gray-200 rounded animate-pulse" />
</div>
</div>
) : (
<>
<div>
<div className="flex w-full items-center gap-x-1">
<Label
small
className="text-sm font-medium text-gray-700"
>
Name
</Label>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
Shows up as the &quot;title&quot; for this
Starter Message. For example, &quot;Write an
email.&quot;
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Field
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
className="mt-1 w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
autoComplete="off"
placeholder="Enter a name..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<ErrorMessage
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<div>
<div className="flex w-full items-center gap-x-1">
<Label
small
className="text-sm font-medium text-gray-700"
>
Message
</Label>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
The actual message to be sent as the initial
user message.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Field
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
className="mt-1 text-sm w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition min-h-[100px] resize-y"
as="textarea"
autoComplete="off"
placeholder="Enter the message..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<ErrorMessage
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
</>
)}
</div>
</div>
))}
</div>
<button
<div className="flex flex-col gap-2">
{values.map((starterMessage, index) => (
<div key={index} className="flex items-center gap-2">
<TextFormField
name={`starter_messages.${index}.message`}
label=""
value={starterMessage.message}
onChange={(e) => handleInputChange(index, e.target.value)}
className="flex-grow"
removeLabel
small
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => {
arrayHelpers.remove(rowIndex * 2 + 1);
arrayHelpers.remove(rowIndex * 2);
arrayHelpers.remove(index);
if (
index === values.length - 2 &&
!values[values.length - 1].message
) {
arrayHelpers.pop();
}
}}
className="p-1.5 bg-white border border-gray-200 rounded-full text-gray-400 hover:text-red-500 hover:border-red-200 transition-colors mt-2"
aria-label="Delete row"
className={`text-gray-400 hover:text-red-500 ${
index === values.length - 1 && !starterMessage.message
? "opacity-50 cursor-not-allowed"
: ""
}`}
disabled={index === values.length - 1 && !starterMessage.message}
>
<FiTrash2 size={14} />
</button>
<FiTrash2 className="h-4 w-4" />
</Button>
</div>
))}
{canAddMore && (
<button
type="button"
onClick={() => {
arrayHelpers.push({
name: "",
message: "",
});
arrayHelpers.push({
name: "",
message: "",
});
}}
className="self-start flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-lg text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-colors"
>
<FiPlus size={16} />
<span>Add Row</span>
</button>
)}
<div className="flex items-center gap-2 ">
<TooltipProvider delayDuration={50}>
<Tooltip onOpenChange={setTooltipOpen} open={tooltipOpen}>
<TooltipTrigger asChild>
<Button
type="button"
size="sm"
onMouseEnter={() => setTooltipOpen(true)}
onMouseLeave={() => setTooltipOpen(false)}
onClick={() => {
const shouldSubmit =
values.filter((msg) => msg.message.trim() !== "").length <
4 &&
!isRefreshing &&
autoStarterMessageEnabled;
if (shouldSubmit) {
debouncedRefreshPrompts();
}
}}
className={`
${
values.filter((msg) => msg.message.trim() !== "").length >=
4 ||
isRefreshing ||
!autoStarterMessageEnabled
? "bg-neutral-800 text-neutral-300 cursor-not-allowed"
: ""
}
`}
>
<div className="flex text-xs items-center gap-x-2">
{isRefreshing ? (
<FiRefreshCw className="w-4 h-4 animate-spin text-white" />
) : (
<SwapIcon className="w-4 h-4 text-white" />
)}
Generate
</div>
</Button>
</TooltipTrigger>
{!autoStarterMessageEnabled && (
<TooltipContent side="top" align="center">
<p className="bg-background-950 max-w-[200px] text-sm p-1.5 text-white">
No LLM providers configured. Generation is not available.
</p>
</TooltipContent>
)}
{values.filter((msg) => msg.message.trim() !== "").length >= 4 && (
<TooltipContent side="top" align="center">
<p className="bg-background-950 max-w-[200px] text-sm p-1.5 text-white">
Max four starter messages
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
</div>
);
}

View File

@@ -22,32 +22,26 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
} else {
body = (
<>
<CardSection>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
admin
defaultPublic={true}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</CardSection>
<div className="mt-12">
<Title>Delete Assistant</Title>
<div className="flex mt-6">
<DeletePersonaButton
personaId={values.existingPersona!.id}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</div>
</div>
<DeletePersonaButton
personaId={values.existingPersona!.id}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</CardSection>
</>
);
}
return (
<div className="w-full">
<BackButton />
<AdminPageTitle title="Edit Assistant" icon={<RobotIcon size={32} />} />
{body}
</div>

View File

@@ -1,10 +1,12 @@
import { ToolSnapshot } from "@/lib/tools/interfaces";
import { DocumentSet, MinimalUserSnapshot } from "@/lib/types";
export interface StarterMessage {
name: string;
export interface StarterMessageBase {
message: string;
}
export interface StarterMessage extends StarterMessageBase {
name: string;
}
export interface Prompt {
id: number;
@@ -42,11 +44,10 @@ export interface Persona {
icon_shape?: number;
icon_color?: string;
uploaded_image_id?: string;
category_id?: number | null;
labels?: PersonaLabel[];
}
export interface PersonaCategory {
export interface PersonaLabel {
id: number;
name: string;
description: string;
}

View File

@@ -23,7 +23,7 @@ interface PersonaCreationRequest {
uploaded_image: File | null;
search_start_date: Date | null;
is_default_persona: boolean;
category_id: number | null;
label_ids?: number[];
}
interface PersonaUpdateRequest {
@@ -49,7 +49,7 @@ interface PersonaUpdateRequest {
remove_image: boolean;
uploaded_image: File | null;
search_start_date: Date | null;
category_id: number | null;
label_ids?: number[];
}
function promptNameFromPersonaName(personaName: string) {
@@ -110,18 +110,18 @@ function updatePrompt({
});
}
export const createPersonaCategory = (name: string, description: string) => {
return fetch("/api/admin/persona/categories", {
export const createPersonaLabel = (name: string) => {
return fetch("/api/persona/labels", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, description }),
body: JSON.stringify({ name }),
});
};
export const deletePersonaCategory = (categoryId: number) => {
return fetch(`/api/admin/persona/category/${categoryId}`, {
export const deletePersonaLabel = (labelId: number) => {
return fetch(`/api/admin/persona/label/${labelId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@@ -129,19 +129,17 @@ export const deletePersonaCategory = (categoryId: number) => {
});
};
export const updatePersonaCategory = (
export const updatePersonaLabel = (
id: number,
name: string,
description: string
) => {
return fetch(`/api/admin/persona/category/${id}`, {
name: string
): Promise<Response> => {
return fetch(`/api/admin/persona/label/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
category_name: name,
category_description: description,
label_name: name,
}),
});
};
@@ -165,7 +163,7 @@ function buildPersonaAPIBody(
icon_shape,
remove_image,
search_start_date,
category_id,
label_ids,
} = creationRequest;
const is_default_persona =
@@ -195,7 +193,7 @@ function buildPersonaAPIBody(
remove_image,
search_start_date,
is_default_persona,
category_id,
label_ids,
};
}

View File

@@ -17,7 +17,7 @@ export default async function Page() {
);
} else {
body = (
<CardSection>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
admin
@@ -28,14 +28,5 @@ export default async function Page() {
);
}
return (
<div className="w-full">
<BackButton />
<AdminPageTitle
title="Create a New Assistant"
icon={<RobotIcon size={32} />}
/>
{body}
</div>
);
return <div className="w-full">{body}</div>;
}

View File

@@ -10,6 +10,7 @@ import {
OpenAIIcon,
GeminiIcon,
OpenSourceIcon,
AnthropicSVG,
} from "@/components/icons/icons";
import { FaRobot } from "react-icons/fa";
@@ -97,7 +98,7 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
return OpenAIIcon; // Default for openai
case "anthropic":
return AnthropicIcon;
return AnthropicSVG;
case "bedrock":
return AWSIcon;
case "azure":

View File

@@ -423,7 +423,7 @@ export function CCPairIndexingStatusTable({
placeholder="Search connectors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="ml-1 w-96 h-9 flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button className="h-9" onClick={() => toggleSources()}>

View File

@@ -1,4 +1,5 @@
import { Layout } from "@/components/admin/Layout";
import { fetchChatData } from "@/lib/chat/fetchChatData";
export default async function AdminLayout({
children,

View File

@@ -5,7 +5,7 @@ import { useState } from "react";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { Button } from "@/components/ui/button";
import { NEXT_PUBLIC_CLOUD_DOMAIN } from "@/lib/constants";
import { NEXT_PUBLIC_WEB_DOMAIN } from "@/lib/constants";
import { ClipboardIcon } from "@/components/icons/icons";
import { Input } from "@/components/ui/input";
import { ThreeDotsLoader } from "@/components/Loading";
@@ -26,11 +26,9 @@ export function AnonymousUserPath({
} = useSWR("/api/tenants/anonymous-user-path", (url) =>
fetch(url)
.then((res) => {
console.log("Response:", res);
return res.json();
})
.then((data) => {
console.log("Data:", data);
return data.anonymous_user_path;
})
);
@@ -118,7 +116,7 @@ export function AnonymousUserPath({
<div className="flex flex-col gap-2 justify-center items-start">
<div className="w-full flex-grow flex items-center rounded-md shadow-sm">
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm h-10">
{NEXT_PUBLIC_CLOUD_DOMAIN}/anonymous/
{NEXT_PUBLIC_WEB_DOMAIN}/anonymous/
</span>
<Input
type="text"
@@ -143,7 +141,7 @@ export function AnonymousUserPath({
className="h-10 px-4"
onClick={() => {
navigator.clipboard.writeText(
`${NEXT_PUBLIC_CLOUD_DOMAIN}/anonymous/${anonymousUserPath}`
`${NEXT_PUBLIC_WEB_DOMAIN}/anonymous/${anonymousUserPath}`
);
setPopup({
message: "Invite link copied!",

View File

@@ -14,14 +14,16 @@ export function AssistantSharedStatusDisplay({
}) {
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
const assistantSharedUsersWithoutOwner = assistant.users?.filter(
const assistantSharedUsersWithoutOwner = (assistant.users || [])?.filter(
(u) => u.id !== assistant.owner?.id
);
if (assistant.is_public) {
return (
<div
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
className={`text-subtle ${
size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"
} flex items-center`}
>
<FiUnlock className="mr-1" />
Public
@@ -32,7 +34,9 @@ export function AssistantSharedStatusDisplay({
if (assistantSharedUsersWithoutOwner.length > 0) {
return (
<div
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
className={`text-subtle ${
size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"
} flex items-center`}
>
<FiUnlock className="mr-1" />
{isOwnedByUser ? (
@@ -61,7 +65,9 @@ export function AssistantSharedStatusDisplay({
return (
<div
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
className={`text-subtle ${
size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"
} flex items-center`}
>
<FiLock className="mr-1" />
Private

View File

@@ -1,19 +0,0 @@
export function AssistantsPageTitle({
children,
}: {
children: JSX.Element | string;
}) {
return (
<h1
className="
text-5xl
font-bold
mb-4
text-center
text-text-900
"
>
{children}
</h1>
);
}

View File

@@ -1,25 +0,0 @@
export function NavigationButton({
children,
}: {
children: JSX.Element | string;
}) {
return (
<div
className="
text-default
py-2
px-4
rounded-full
border-2
border-border
hover:bg-hover
focus:outline-none
focus:ring-2
focus:ring-border
focus:ring-opacity-50
"
>
{children}
</div>
);
}

View File

@@ -1,28 +0,0 @@
import { Badge } from "@/components/ui/badge";
import { PersonaCategory as PersonaCategoryType } from "../admin/assistants/interfaces";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export default function PersonaCategory({
personaCategory,
}: {
personaCategory: PersonaCategoryType;
}) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="cursor-help">
<Badge variant="purple">{personaCategory.name}</Badge>
</TooltipTrigger>
<TooltipContent>
<p>{personaCategory.description}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -1,9 +1,5 @@
"use client";
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
import { ChatSession } from "@/app/chat/interfaces";
import { Folder } from "@/app/chat/folders/interfaces";
import { User } from "@/lib/types";
import Cookies from "js-cookie";
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
import {
@@ -21,35 +17,26 @@ import { pageType } from "../chat/sessionSidebar/types";
import FixedLogo from "../chat/shared_chat_search/FixedLogo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useChatContext } from "@/components/context/ChatContext";
import { HistorySidebar } from "../chat/sessionSidebar/HistorySidebar";
import { useAssistants } from "@/components/context/AssistantsContext";
import AssistantModal from "./mine/AssistantModal";
interface SidebarWrapperProps<T extends object> {
initiallyToggled: boolean;
page: pageType;
size?: "sm" | "lg";
children: ReactNode;
}
export default function SidebarWrapper<T extends object>({
initiallyToggled,
page,
size = "sm",
children,
}: SidebarWrapperProps<T>) {
const { chatSessions, folders, openedFolders } = useChatContext();
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
const [showDocSidebar, setShowDocSidebar] = useState(false); // State to track if sidebar is open
// Used to maintain a "time out" for history sidebar so our existing refs can have time to process change
const [untoggled, setUntoggled] = useState(false);
const explicitlyUntoggle = () => {
setShowDocSidebar(false);
setUntoggled(true);
setTimeout(() => {
setUntoggled(false);
}, 200);
};
const toggleSidebar = useCallback(() => {
Cookies.set(
SIDEBAR_TOGGLED_COOKIE_NAME,
@@ -62,6 +49,16 @@ export default function SidebarWrapper<T extends object>({
}, [toggledSidebar]);
const sidebarElementRef = useRef<HTMLDivElement>(null);
const { folders, openedFolders, chatSessions } = useChatContext();
const { assistants } = useAssistants();
const explicitlyUntoggle = () => {
setShowDocSidebar(false);
setUntoggled(true);
setTimeout(() => {
setUntoggled(false);
}, 200);
};
const settings = useContext(SettingsContext);
useSidebarVisibility({
@@ -72,7 +69,7 @@ export default function SidebarWrapper<T extends object>({
mobile: settings?.isMobile,
});
const innerSidebarElementRef = useRef<HTMLDivElement>(null);
const [showAssistantsModal, setShowAssistantsModal] = useState(false);
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -94,6 +91,9 @@ export default function SidebarWrapper<T extends object>({
return (
<div className="flex relative overflow-x-hidden overscroll-contain flex-col w-full h-screen">
{showAssistantsModal && (
<AssistantModal hideModal={() => setShowAssistantsModal(false)} />
)}
<div
ref={sidebarElementRef}
className={`
@@ -114,10 +114,13 @@ export default function SidebarWrapper<T extends object>({
}`}
>
<div className="w-full relative">
{" "}
<HistorySidebar
page={page}
setShowAssistantsModal={setShowAssistantsModal}
assistants={assistants}
page={"chat"}
explicitlyUntoggle={explicitlyUntoggle}
ref={innerSidebarElementRef}
ref={sidebarElementRef}
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
existingChats={chatSessions}
@@ -128,11 +131,11 @@ export default function SidebarWrapper<T extends object>({
</div>
</div>
<div className="absolute h-svh px-2 left-0 w-full top-0">
<div className="absolute px-2 left-0 w-full top-0">
<FunctionalHeader
sidebarToggled={toggledSidebar}
toggleSidebar={toggleSidebar}
page="assistants"
page="chat"
/>
<div className="w-full flex">
<div

View File

@@ -1,66 +1,35 @@
import { ErrorCallout } from "@/components/ErrorCallout";
import Text from "@/components/ui/text";
import CardSection from "@/components/admin/CardSection";
import { HeaderWrapper } from "@/components/header/HeaderWrapper";
import { AssistantEditor } from "@/app/admin/assistants/AssistantEditor";
import { SuccessfulPersonaUpdateRedirectType } from "@/app/admin/assistants/enums";
import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
import { DeletePersonaButton } from "@/app/admin/assistants/[id]/DeletePersonaButton";
import { LargeBackButton } from "../../LargeBackButton";
import Title from "@/components/ui/title";
import { BackButton } from "@/components/BackButton";
export default async function Page(props: { params: Promise<{ id: string }> }) {
const params = await props.params;
const [values, error] = await fetchAssistantEditorInfoSS(params.id);
let body;
if (!values) {
body = (
return (
<div className="px-32">
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
</div>
);
} else {
body = (
<div className="w-full my-16">
return (
<div className="w-full py-8">
<div className="px-32">
<div className="mx-auto container">
<CardSection>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
defaultPublic={false}
redirectType={SuccessfulPersonaUpdateRedirectType.CHAT}
/>
</CardSection>
<Title className="mt-12">Delete Assistant</Title>
<Text>
Click the button below to permanently delete this assistant.
</Text>
<div className="flex mt-6">
<DeletePersonaButton
personaId={values.existingPersona!.id}
redirectType={SuccessfulPersonaUpdateRedirectType.CHAT}
/>
</div>
</div>
</div>
</div>
);
}
return (
<div>
<HeaderWrapper>
<div className="h-full flex flex-col">
<div className="flex my-auto">
<LargeBackButton />
<h1 className="flex text-xl text-strong font-bold my-auto">
Edit Assistant
</h1>
</div>
</div>
</HeaderWrapper>
{body}
</div>
);
}

View File

@@ -1,405 +0,0 @@
"use client";
import {
Persona,
PersonaCategory as PersonaCategoryType,
} from "@/app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { User } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { FiList, FiMinus, FiPlus } from "react-icons/fi";
import { AssistantsPageTitle } from "../AssistantsPageTitle";
import {
addAssistantToList,
removeAssistantFromList,
} from "@/lib/assistants/updateAssistantPreferences";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { AssistantTools } from "../ToolsDisplay";
import { classifyAssistants } from "@/lib/assistants/utils";
import { useAssistants } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider";
import PersonaCategory from "../PersonaCategory";
import { useCategories } from "@/lib/hooks";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export function AssistantGalleryCard({
onlyAssistant,
assistant,
user,
setPopup,
selectedAssistant,
}: {
onlyAssistant: boolean;
assistant: Persona;
user: User | null;
setPopup: (popup: PopupSpec) => void;
selectedAssistant: boolean;
}) {
const { data: categories } = useCategories();
const { refreshUser } = useUser();
return (
<div
key={assistant.id}
className="
bg-background-emphasis
rounded-lg
shadow-md
p-4
"
>
<div className="flex items-center">
<AssistantIcon assistant={assistant} />
<h2
className="
text-xl
font-semibold
my-auto
ml-2
text-strong
line-clamp-2
"
>
{assistant.name}
</h2>
{user && (
<div className="ml-auto">
{selectedAssistant ? (
<Button
className="
mr-2
my-auto
bg-background-700
hover:bg-background-600
"
icon={FiMinus}
onClick={async () => {
if (onlyAssistant) {
setPopup({
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
type: "error",
});
return;
}
const success = await removeAssistantFromList(assistant.id);
if (success) {
setPopup({
message: `"${assistant.name}" has been removed from your list.`,
type: "success",
});
await refreshUser();
} else {
setPopup({
message: `"${assistant.name}" could not be removed from your list.`,
type: "error",
});
}
}}
size="sm"
variant="destructive"
>
Deselect
</Button>
) : (
<Button
className="
mr-2
my-auto
bg-accent
hover:bg-accent-hover
"
icon={FiPlus}
onClick={async () => {
const success = await addAssistantToList(assistant.id);
if (success) {
setPopup({
message: `"${assistant.name}" has been added to your list.`,
type: "success",
});
await refreshUser();
} else {
setPopup({
message: `"${assistant.name}" could not be added to your list.`,
type: "error",
});
}
}}
size="sm"
variant="submit"
>
Add
</Button>
)}
</div>
)}
</div>
<p className="text-sm mt-2">{assistant.description}</p>
<p className="text-subtle text-sm my-2">
Author: {assistant.owner?.email || "Onyx"}
</p>
{assistant.tools.length > 0 && (
<AssistantTools list assistant={assistant} />
)}
{assistant.category_id && categories && (
<PersonaCategory
personaCategory={
categories?.find(
(category: PersonaCategoryType) =>
category.id === assistant.category_id
)!
}
/>
)}
</div>
);
}
export function AssistantsGallery() {
const { assistants } = useAssistants();
const { user } = useUser();
const { data: categories } = useCategories();
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const { popup, setPopup } = usePopup();
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
user,
assistants
);
const defaultAssistants = assistants
.filter((assistant) => assistant.is_default_persona)
.filter(
(assistant) =>
(assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
assistant.description
.toLowerCase()
.includes(searchQuery.toLowerCase())) &&
(selectedCategory === null ||
selectedCategory === assistant.category_id)
);
const nonDefaultAssistants = assistants
.filter((assistant) => !assistant.is_default_persona)
.filter(
(assistant) =>
(assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
assistant.description
.toLowerCase()
.includes(searchQuery.toLowerCase())) &&
(selectedCategory === null ||
selectedCategory === assistant.category_id)
);
return (
<>
{popup}
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<AssistantsPageTitle>Assistant Gallery</AssistantsPageTitle>
<div className="grid grid-cols-2 gap-4 mt-4 mb-6">
<Button
onClick={() => router.push("/assistants/new")}
variant="default"
className="p-6 text-base"
icon={FiPlus}
>
Create New Assistant
</Button>
<Button
onClick={() => router.push("/assistants/mine")}
variant="outline"
className="text-base py-6"
icon={FiList}
>
Your Assistants
</Button>
</div>
<div className="mt-4 mb-6">
<div className="relative">
<input
type="text"
placeholder="Search assistants..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="
w-full
py-3
px-4
pl-10
text-lg
border-2
border-background-strong
rounded-full
bg-background-50
text-text-700
placeholder-text-400
focus:outline-none
focus:ring-2
focus:ring-primary-500
focus:border-transparent
transition duration-300 ease-in-out
"
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-text-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
</div>
</div>
{categories && categories?.length > 0 && (
<div className="mb-8">
<Select
value={selectedCategory?.toString() || "all"}
onValueChange={(value) =>
setSelectedCategory(value === "all" ? null : parseInt(value))
}
>
<SelectTrigger
className="
w-[240px]
bg-background
border-2
border-background-strong
text-text-500
rounded-lg
shadow-sm
hover:bg-background-emphasis
hover:border-primary-500/50
hover:text-primary-500
transition-all
duration-200
"
>
<SelectValue placeholder="Filter by category..." />
</SelectTrigger>
<SelectContent className="bg-background border-background-strong">
<SelectGroup>
<SelectLabel className="text-sm font-medium text-text-400">
Categories
</SelectLabel>
<SelectItem
value="all"
className="cursor-pointer hover:bg-background-emphasis"
>
All Categories
</SelectItem>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
className="cursor-pointer hover:bg-background-emphasis"
>
{category.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
{defaultAssistants.length == 0 &&
nonDefaultAssistants.length == 0 &&
assistants.length != 0 && (
<div className="text-text-500">
No assistants found for this search
</div>
)}
{defaultAssistants.length > 0 && (
<>
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-2 text-text-900">
Default Assistants
</h2>
<h3 className="text-lg text-text-500">
These are assistant created by your admins are and preferred.
</h3>
</section>
<div
className="
w-full
grid
grid-cols-2
gap-4
py-2
"
>
{defaultAssistants.map((assistant) => (
<AssistantGalleryCard
onlyAssistant={visibleAssistants.length === 1}
selectedAssistant={visibleAssistants.includes(assistant)}
key={assistant.id}
assistant={assistant}
user={user}
setPopup={setPopup}
/>
))}
</div>
</>
)}
{nonDefaultAssistants.length > 0 && (
<section className="mt-12 mb-8 flex flex-col gap-y-2">
<div className="flex flex-col">
<h2 className="text-2xl font-semibold text-text-900">
Other Assistants
</h2>
<h3 className="text-lg text-text-500">
These are community-contributed assistants.
</h3>
</div>
<div
className="
w-full
grid
grid-cols-2
gap-4
py-2
"
>
{nonDefaultAssistants.map((assistant) => (
<AssistantGalleryCard
onlyAssistant={visibleAssistants.length === 1}
selectedAssistant={visibleAssistants.includes(assistant)}
key={assistant.id}
assistant={assistant}
user={user}
setPopup={setPopup}
/>
))}
</div>
</section>
)}
</div>
</>
);
}

View File

@@ -1,16 +0,0 @@
"use client";
import SidebarWrapper from "../SidebarWrapper";
import { AssistantsGallery } from "./AssistantsGallery";
export default function WrappedAssistantsGallery({
toggleSidebar,
}: {
toggleSidebar: boolean;
}) {
return (
<SidebarWrapper page="chat" initiallyToggled={toggleSidebar}>
<AssistantsGallery />
</SidebarWrapper>
);
}

View File

@@ -1,64 +0,0 @@
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { unstable_noStore as noStore } from "next/cache";
import { redirect } from "next/navigation";
import WrappedAssistantsGallery from "./WrappedAssistantsGallery";
import { cookies } from "next/headers";
import { ChatProvider } from "@/components/context/ChatContext";
export default async function GalleryPage(props: {
searchParams: Promise<{ [key: string]: string }>;
}) {
noStore();
const searchParams = await props.searchParams;
const requestCookies = await cookies();
const data = await fetchChatData(searchParams);
if ("redirect" in data) {
redirect(data.redirect);
}
const {
user,
chatSessions,
folders,
openedFolders,
toggleSidebar,
shouldShowWelcomeModal,
availableSources,
ccPairs,
documentSets,
tags,
llmProviders,
defaultAssistantId,
} = data;
return (
<ChatProvider
value={{
chatSessions,
availableSources,
ccPairs,
documentSets,
tags,
availableDocumentSets: documentSets,
availableTags: tags,
llmProviders,
folders,
openedFolders,
shouldShowWelcomeModal,
defaultAssistantId,
}}
>
{shouldShowWelcomeModal && (
<WelcomeModal user={user} requestCookies={requestCookies} />
)}
<InstantSSRAutoRefresh />
<WrappedAssistantsGallery toggleSidebar={toggleSidebar} />
</ChatProvider>
);
}

View File

@@ -0,0 +1,328 @@
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import {
FiMoreHorizontal,
FiShare2,
FiEye,
FiEyeOff,
FiTrash,
FiEdit,
FiHash,
FiBarChart,
FiLock,
FiUnlock,
FiSearch,
} from "react-icons/fi";
import { FaHashtag } from "react-icons/fa";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { AssistantVisibilityPopover } from "./AssistantVisibilityPopover";
import { DeleteAssistantPopover } from "./DeleteAssistantPopover";
import { Persona } from "@/app/admin/assistants/interfaces";
import { useUser } from "@/components/user/UserProvider";
import { useAssistants } from "@/components/context/AssistantsContext";
import { checkUserOwnsAssistant } from "@/lib/assistants/utils";
import { toggleAssistantPinnedStatus } from "@/lib/assistants/pinnedAssistants";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { PinnedIcon } from "@/components/icons/icons";
import {
deletePersona,
togglePersonaPublicStatus,
} from "@/app/admin/assistants/lib";
import { HammerIcon } from "lucide-react";
export const AssistantBadge = ({
text,
className,
}: {
text: string;
className?: string;
}) => {
return (
<div
className={`h-4 px-1.5 py-1 text-[10px] bg-[#e6e3dd]/50 rounded-lg justify-center items-center gap-1 inline-flex ${className}`}
>
<div className="text-[#4a4a4a] font-normal leading-[8px]">{text}</div>
</div>
);
};
const AssistantCard: React.FC<{
persona: Persona;
pinned: boolean;
closeModal: () => void;
}> = ({ persona, pinned, closeModal }) => {
const { user, refreshUser } = useUser();
const router = useRouter();
const { refreshAssistants } = useAssistants();
const isOwnedByUser = checkUserOwnsAssistant(user, persona);
const [activePopover, setActivePopover] = useState<string | null | undefined>(
undefined
);
const handleShare = () => setActivePopover("visibility");
const handleDelete = () => setActivePopover("delete");
const handleEdit = () => {
router.push(`/assistants/edit/${persona.id}`);
setActivePopover(null);
};
const closePopover = () => setActivePopover(undefined);
return (
<div className="w-full p-2 overflow-visible pb-4 pt-3 bg-[#fefcf9] rounded shadow-[0px_0px_4px_0px_rgba(0,0,0,0.25)] flex flex-col">
<div className="w-full flex">
<div className="ml-2 mr-4 mt-1 w-8 h-8">
<AssistantIcon assistant={persona} size="large" />
</div>
<div className="flex-1 mt-1 flex flex-col">
<div className="flex justify-between items-start mb-1">
<div className="flex items-end gap-x-2 leading-none">
<h3 className="text-black leading-none font-semibold text-base lg-normal">
{persona.name}
</h3>
{persona.labels && persona.labels.length > 0 && (
<>
{persona.labels.slice(0, 3).map((label, index) => (
<AssistantBadge key={index} text={label.name} />
))}
{persona.labels.length > 3 && (
<AssistantBadge
text={`+${persona.labels.length - 3} more`}
/>
)}
</>
)}
</div>
{isOwnedByUser && (
<div className="flex items-center gap-x-2">
<Popover
open={activePopover !== undefined}
onOpenChange={(open) =>
open ? setActivePopover(null) : setActivePopover(undefined)
}
>
<PopoverTrigger asChild>
<button
type="button"
className="hover:bg-neutral-100 p-1 -my-1 rounded-full"
>
<FiMoreHorizontal size={16} />
</button>
</PopoverTrigger>
<PopoverContent
className={`z-[10000] ${
activePopover === null ? "w-32" : "w-80"
} p-2`}
>
{activePopover === null && (
<div className="flex flex-col text-sm space-y-1">
<button
onClick={isOwnedByUser ? handleEdit : undefined}
className={`w-full flex items-center text-left px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-100"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiEdit size={12} className="inline mr-2" />
Edit
</button>
{/*
<button
onClick={isOwnedByUser ? handleShare : undefined}
className={`w-full text-left flex items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-100"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiShare2 size={12} className="inline mr-2" />
Share
</button> */}
<button
onClick={
isOwnedByUser
? () => {
router.push(
`/assistants/stats/${persona.id}`
);
closePopover();
}
: undefined
}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-100"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiBarChart size={12} className="inline mr-2" />
Stats
</button>
<button
onClick={isOwnedByUser ? handleDelete : undefined}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-100 text-red-600"
: "opacity-50 cursor-not-allowed text-red-300"
}`}
disabled={!isOwnedByUser}
>
<FiTrash size={12} className="inline mr-2" />
Delete
</button>
</div>
)}
{activePopover === "visibility" && (
<AssistantVisibilityPopover
assistant={persona}
user={user}
allUsers={[]}
onClose={closePopover}
onTogglePublic={async (isPublic: boolean) => {
await togglePersonaPublicStatus(persona.id, isPublic);
await refreshAssistants();
}}
/>
)}
{activePopover === "delete" && (
<DeleteAssistantPopover
entityName={persona.name}
onClose={closePopover}
onSubmit={async () => {
const success = await deletePersona(persona.id);
if (success) {
await refreshAssistants();
}
closePopover();
}}
/>
)}
</PopoverContent>
</Popover>
</div>
)}
</div>
<p className="text-black font-[350] mt-0 text-sm mb-1 line-clamp-2 h-[2.7em]">
{persona.description || "\u00A0"}
</p>
<div className="flex flex-col ">
{/* <div className="mb-1 mt-1">
<div className="flex items-center">
</div>
</div> */}
<div className="my-1">
<span className="flex items-center text-black text-xs opacity-50">
{(persona.owner?.email || persona.builtin_persona) && "By "}
{persona.owner?.email || (persona.builtin_persona && "Onyx")}
{(persona.owner?.email || persona.builtin_persona) && (
<span className="mx-2"></span>
)}
{persona.tools.length > 0 ? (
<>
{persona.tools.length}
{" Action"}
{persona.tools.length !== 1 ? "s" : ""}
</>
) : (
"No Actions"
)}
<span className="mx-2"></span>
{persona.is_public ? (
<>
<FiUnlock size={12} className="inline mr-1" />
Public
</>
) : (
<>
<FiLock size={12} className="inline mr-1" />
Private
</>
)}
</span>
</div>
<div className="mb-1 flex flex-wrap">
{persona.document_sets.slice(0, 5).map((set, index) => (
<AssistantBadge
className="!text-base"
key={index}
text={set.name}
/>
))}
</div>
</div>
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => {
router.push(`/chat?assistantId=${persona.id}`);
closeModal();
}}
className="hover:bg-neutral-100 hover:text-text px-2 py-1 gap-x-1 rounded border border-black flex items-center"
>
<FaHashtag size={12} className="flex-none" />
<span className="text-xs">Start Chat</span>
</button>
</TooltipTrigger>
<TooltipContent>
Start a new chat with this assistant
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={async () => {
await toggleAssistantPinnedStatus(
user?.preferences.pinned_assistants || [],
persona.id,
!pinned
);
await refreshUser();
}}
className="hover:bg-neutral-100 px-2 py-1 gap-x-1 rounded border border-black flex items-center"
style={{ width: "65px" }}
>
<PinnedIcon size={12} />
<p className="text-xs">{pinned ? "Unpin" : "Pin"}</p>
</button>
</TooltipTrigger>
<TooltipContent>
{pinned ? "Remove from" : "Add to"} your pinned list
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
<div className="flex items-center justify-center"></div>
</div>
);
};
export default AssistantCard;

View File

@@ -0,0 +1,237 @@
"use client";
import React, { useMemo, useState, useEffect } from "react";
import { Persona } from "@/app/admin/assistants/interfaces";
import { useRouter } from "next/navigation";
import { Modal } from "@/components/Modal";
import AssistantCard from "./AssistantCard";
import { useAssistants } from "@/components/context/AssistantsContext";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { useUser } from "@/components/user/UserProvider";
import { Button } from "@/components/ui/button";
import { useLabels } from "@/lib/hooks";
export const AssistantBadgeSelector = ({
text,
selected,
toggleFilter,
}: {
text: string;
selected: boolean;
toggleFilter: () => void;
}) => {
return (
<div
className={`${
selected
? "bg-neutral-900 text-white"
: "bg-transparent text-neutral-900"
} h-5 px-1 py-0.5 rounded-lg cursor-pointer text-[12px] font-normal leading-[10px] border border-black justify-center items-center gap-1 inline-flex`}
onClick={toggleFilter}
>
{text}
</div>
);
};
export enum AssistantFilter {
Pinned = "Pinned",
Public = "Public",
Private = "Private",
}
const useAssistantFilter = () => {
const [assistantFilters, setAssistantFilters] = useState<
Record<AssistantFilter, boolean>
>({
[AssistantFilter.Pinned]: false,
[AssistantFilter.Public]: false,
[AssistantFilter.Private]: false,
});
const toggleAssistantFilter = (filter: AssistantFilter) => {
setAssistantFilters((prevFilters) => ({
...prevFilters,
[filter]: !prevFilters[filter],
}));
};
return { assistantFilters, toggleAssistantFilter, setAssistantFilters };
};
export default function AssistantModal({
hideModal,
}: {
hideModal: () => void;
}) {
const [showAllFeaturedAssistants, setShowAllFeaturedAssistants] =
useState(false);
const { assistants, visibleAssistants, pinnedAssistants } = useAssistants();
const { assistantFilters, toggleAssistantFilter, setAssistantFilters } =
useAssistantFilter();
const router = useRouter();
const { user } = useUser();
const [searchQuery, setSearchQuery] = useState("");
const [isSearchFocused, setIsSearchFocused] = useState(false);
const memoizedCurrentlyVisibleAssistants = useMemo(() => {
return assistants.filter((assistant) => {
const nameMatches = assistant.name
.toLowerCase()
.includes(searchQuery.toLowerCase());
const labelMatches = assistant.labels?.some((label) =>
label.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const publicFilter =
!assistantFilters[AssistantFilter.Public] || assistant.is_public;
const privateFilter =
!assistantFilters[AssistantFilter.Private] || !assistant.is_public;
const pinnedFilter =
!assistantFilters[AssistantFilter.Pinned] ||
pinnedAssistants.map((a: Persona) => a.id).includes(assistant.id);
return (
(nameMatches || labelMatches) &&
publicFilter &&
privateFilter &&
pinnedFilter
);
});
}, [assistants, searchQuery, assistantFilters, pinnedAssistants]);
const featuredAssistants = [
...memoizedCurrentlyVisibleAssistants.filter(
(assistant) => assistant.builtin_persona || assistant.is_default_persona
),
];
const allAssistants = memoizedCurrentlyVisibleAssistants.filter(
(assistant) => !assistant.builtin_persona && !assistant.is_default_persona
);
const maxHeight = 900;
const calculatedHeight = Math.min(
Math.ceil(assistants.length / 2) * 170 + 200,
window.innerHeight * 0.8
);
const height = Math.min(calculatedHeight, maxHeight);
return (
<Modal
heightOverride={`${height}px`}
onOutsideClick={hideModal}
removeBottomPadding
className={`max-w-4xl ${height} w-[95%] overflow-hidden`}
>
<div className="flex flex-col h-full">
<div className="flex flex-col sticky top-0 z-10">
<div className="flex px-2 justify-between items-center gap-x-2 mb-0">
<div className="h-12 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
<div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-[#dcdad4] flex items-center px-3">
{!isSearchFocused && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
)}
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
type="text"
className="w-full h-full bg-transparent outline-none text-black"
/>
</div>
</div>
<button
onClick={() => router.push("/assistants/new")}
className="h-10 cursor-pointer px-6 py-3 bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-[#fffcf4] text-lg font-normal leading-normal">
Create
</div>
</button>
</div>
<div className="px-2 flex py-2 items-center gap-x-2 mb-2 flex-wrap">
<AssistantBadgeSelector
text="Pinned"
selected={assistantFilters[AssistantFilter.Pinned]}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Pinned)}
/>
<AssistantBadgeSelector
text="Public"
selected={assistantFilters[AssistantFilter.Public]}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Public)}
/>
<AssistantBadgeSelector
text="Private"
selected={assistantFilters[AssistantFilter.Private]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Private)
}
/>
</div>
<div className="w-full border-t border-neutral-200" />
</div>
<div className="flex-grow overflow-y-auto">
<h2 className="text-2xl font-semibold text-gray-800 mb-2 px-4 py-2">
Featured Assistants
</h2>
<div className="w-full px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{featuredAssistants.length > 0 ? (
featuredAssistants.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={pinnedAssistants.includes(assistant)}
persona={assistant}
closeModal={hideModal}
/>
</div>
))
) : (
<div className="col-span-2 text-center text-gray-500">
No featured assistants match filters
</div>
)}
</div>
{allAssistants && allAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-gray-800 mt-4 mb-2 px-4 py-2">
All Assistants
</h2>
<div className="w-full mt-2 px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{allAssistants
.sort((a, b) => b.id - a.id)
.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={pinnedAssistants.includes(assistant)}
persona={assistant}
closeModal={hideModal}
/>
</div>
))}
</div>
</>
)}
</div>
</div>
</Modal>
);
}

View File

@@ -39,7 +39,7 @@ export function AssistantSharingModal({
const [selectedUsers, setSelectedUsers] = useState<MinimalUserSnapshot[]>([]);
const assistantName = assistant.name;
const sharedUsersWithoutOwner = assistant.users.filter(
const sharedUsersWithoutOwner = (assistant.users || [])?.filter(
(u) => u.id !== assistant.owner?.id
);

View File

@@ -0,0 +1,213 @@
import React, { useState } from "react";
import { MinimalUserSnapshot, User } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { FiPlus, FiX } from "react-icons/fi";
import { Persona } from "@/app/admin/assistants/interfaces";
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
import { UsersIcon } from "@/components/icons/icons";
import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
import {
addUsersToAssistantSharedList,
removeUsersFromAssistantSharedList,
} from "@/lib/assistants/shareAssistant";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Bubble } from "@/components/Bubble";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Spinner } from "@/components/Spinner";
import { useAssistants } from "@/components/context/AssistantsContext";
interface AssistantSharingPopoverProps {
assistant: Persona;
user: User | null;
allUsers: MinimalUserSnapshot[];
onClose: () => void;
}
export function AssistantSharingPopover({
assistant,
user,
allUsers,
onClose,
}: AssistantSharingPopoverProps) {
const { refreshAssistants } = useAssistants();
const { popup, setPopup } = usePopup();
const [isUpdating, setIsUpdating] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<MinimalUserSnapshot[]>([]);
const assistantName = assistant.name;
const sharedUsersWithoutOwner = (assistant.users || [])?.filter(
(u) => u.id !== assistant.owner?.id
);
const handleShare = async () => {
setIsUpdating(true);
const startTime = Date.now();
const error = await addUsersToAssistantSharedList(
assistant,
selectedUsers.map((user) => user.id)
);
await refreshAssistants();
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, 1000 - elapsedTime);
setTimeout(() => {
setIsUpdating(false);
if (error) {
setPopup({
message: `Failed to share assistant - ${error}`,
type: "error",
});
}
}, remainingTime);
};
let sharedStatus = null;
if (assistant.is_public || !sharedUsersWithoutOwner.length) {
sharedStatus = (
<AssistantSharedStatusDisplay
size="md"
assistant={assistant}
user={user}
/>
);
} else {
sharedStatus = (
<div>
Shared with:{" "}
<div className="flex flex-wrap gap-x-2 mt-2">
{sharedUsersWithoutOwner.map((u) => (
<Bubble
key={u.id}
isSelected={false}
onClick={async () => {
setIsUpdating(true);
const startTime = Date.now();
const error = await removeUsersFromAssistantSharedList(
assistant,
[u.id]
);
await refreshAssistants();
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, 1000 - elapsedTime);
setTimeout(() => {
setIsUpdating(false);
if (error) {
setPopup({
message: `Failed to remove assistant - ${error}`,
type: "error",
});
}
}, remainingTime);
}}
>
<div className="flex">
{u.email} <FiX className="ml-1 my-auto" />
</div>
</Bubble>
))}
</div>
</div>
);
}
return (
<>
{popup}
<div>
<div className="flex items-end space-x-3 mb-4">
<AssistantIcon size="large" assistant={assistant} />
<h2 className="text-xl text-text-800 font-semibold">
{assistantName}
</h2>
</div>
<p className="text-text-600 text-sm mb-4">
Manage access to this assistant by sharing it with other users.
</p>
<div className="mb-4">
<h3 className="text-sm font-semibold mb-2">Current Status</h3>
<div className="bg-gray-50 rounded-lg p-2">{sharedStatus}</div>
</div>
<div className="mb-4">
<h3 className="text-sm font-semibold mb-2">Share Assistant</h3>
<SearchMultiSelectDropdown
options={allUsers
.filter(
(u1) =>
!selectedUsers.map((u2) => u2.id).includes(u1.id) &&
!sharedUsersWithoutOwner.map((u2) => u2.id).includes(u1.id) &&
u1.id !== user?.id
)
.map((user) => ({
name: user.email,
value: user.id,
}))}
onSelect={(option) => {
setSelectedUsers([
...Array.from(
new Set([
...selectedUsers,
{ id: option.value as string, email: option.name },
])
),
]);
}}
itemComponent={({ option }) => (
<div className="flex items-center px-4 py-2.5 cursor-pointer hover:bg-gray-100">
<UsersIcon className="mr-3 text-gray-500" />
<span className="flex-grow">{option.name}</span>
<FiPlus className="text-blue-500" />
</div>
)}
/>
</div>
{selectedUsers.length > 0 && (
<div className="mb-4">
<h4 className="text-xs font-medium text-gray-700 mb-2">
Selected Users:
</h4>
<div className="flex flex-wrap gap-2">
{selectedUsers.map((selectedUser) => (
<div
key={selectedUser.id}
onClick={() => {
setSelectedUsers(
selectedUsers.filter(
(user) => user.id !== selectedUser.id
)
);
}}
className="flex items-center bg-blue-50 text-blue-700 rounded-full px-3 py-1 text-xs hover:bg-blue-100 transition-colors duration-200 cursor-pointer"
>
{selectedUser.email}
<FiX className="ml-2 text-blue-500" />
</div>
))}
</div>
</div>
)}
{selectedUsers.length > 0 && (
<Button
onClick={() => {
handleShare();
setSelectedUsers([]);
}}
size="sm"
variant="secondary"
>
Share with Selected Users
</Button>
)}
</div>
{isUpdating && <Spinner />}
</>
);
}

View File

@@ -0,0 +1,198 @@
import React, { useState } from "react";
import { MinimalUserSnapshot, User } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { FiPlus, FiX, FiEye, FiEyeOff } from "react-icons/fi";
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
import { UsersIcon } from "@/components/icons/icons";
import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
import {
addUsersToAssistantSharedList,
removeUsersFromAssistantSharedList,
} from "@/lib/assistants/shareAssistant";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Bubble } from "@/components/Bubble";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Spinner } from "@/components/Spinner";
import { useAssistants } from "@/components/context/AssistantsContext";
import { Separator } from "@/components/ui/separator";
import { Persona } from "@/app/admin/assistants/interfaces";
import { ThreeDotsLoader } from "@/components/Loading";
interface AssistantVisibilityPopoverProps {
assistant: Persona;
user: User | null;
allUsers: MinimalUserSnapshot[];
onClose: () => void;
onTogglePublic: (isPublic: boolean) => Promise<void>;
}
export function AssistantVisibilityPopover({
assistant,
user,
allUsers,
onClose,
onTogglePublic,
}: AssistantVisibilityPopoverProps) {
const { refreshAssistants } = useAssistants();
const { popup, setPopup } = usePopup();
const [isUpdating, setIsUpdating] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<MinimalUserSnapshot[]>([]);
const assistantName = assistant.name;
const sharedUsersWithoutOwner = (assistant.users || [])?.filter(
(u: MinimalUserSnapshot) => u.id !== assistant.owner?.id
);
const handleShare = async () => {
setIsUpdating(true);
const startTime = Date.now();
const error = await addUsersToAssistantSharedList(
assistant,
selectedUsers.map((user) => user.id)
);
await refreshAssistants();
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, 1000 - elapsedTime);
setTimeout(() => {
setIsUpdating(false);
if (error) {
setPopup({
message: `Failed to share assistant - ${error}`,
type: "error",
});
}
}, remainingTime);
};
const handleTogglePublic = async () => {
setIsUpdating(true);
await onTogglePublic(!assistant.is_public);
setIsUpdating(false);
};
return (
<>
{popup}
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold mb-2">Visibility</h3>
<Button
onClick={handleTogglePublic}
variant="outline"
size="sm"
className="w-full justify-start"
>
{assistant.is_public ? (
<>
<FiEyeOff className="mr-2" />
Make Private
</>
) : (
<>
<FiEye className="mr-2" />
Make Public
</>
)}
{isUpdating && (
<div className="ml-2 inline-flex items-center">
<ThreeDotsLoader />
<span className="ml-2 text-sm text-gray-600">Updating...</span>
</div>
)}
</Button>
</div>
<Separator />
<div>
<h3 className="text-sm font-semibold mb-2">Share</h3>
<SearchMultiSelectDropdown
options={allUsers
.filter(
(u1) =>
!selectedUsers.map((u2) => u2.id).includes(u1.id) &&
!sharedUsersWithoutOwner
.map((u2: MinimalUserSnapshot) => u2.id)
.includes(u1.id) &&
u1.id !== user?.id
)
.map((user) => ({
name: user.email,
value: user.id,
}))}
onSelect={(option) => {
setSelectedUsers([
...Array.from(
new Set([
...selectedUsers,
{ id: option.value as string, email: option.name },
])
),
]);
}}
itemComponent={({ option }) => (
<div className="flex items-center px-4 py-2.5 cursor-pointer hover:bg-gray-100">
<UsersIcon className="mr-3 text-gray-500" />
<span className="flex-grow">{option.name}</span>
<FiPlus className="text-blue-500" />
</div>
)}
/>
</div>
{selectedUsers.length > 0 && (
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">
Selected Users:
</h4>
<div className="flex flex-wrap gap-2">
{selectedUsers.map((selectedUser) => (
<div
key={selectedUser.id}
onClick={() => {
setSelectedUsers(
selectedUsers.filter(
(user) => user.id !== selectedUser.id
)
);
}}
className="flex items-center bg-blue-50 text-blue-700 rounded-full px-3 py-1 text-xs hover:bg-blue-100 transition-colors duration-200 cursor-pointer"
>
{selectedUser.email}
<FiX className="ml-2 text-blue-500" />
</div>
))}
</div>
</div>
)}
{selectedUsers.length > 0 && (
<Button
onClick={() => {
handleShare();
setSelectedUsers([]);
}}
size="sm"
variant="secondary"
>
Share with Selected Users
</Button>
)}
<div>
<h3 className="text-sm font-semibold mb-2">Currently Shared With</h3>
<div className="bg-gray-50 rounded-lg p-2">
<AssistantSharedStatusDisplay
size="md"
assistant={assistant}
user={user}
/>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,498 +0,0 @@
"use client";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { MinimalUserSnapshot, User } from "@/lib/types";
import { Persona } from "@/app/admin/assistants/interfaces";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
FiBarChart,
FiEdit2,
FiList,
FiMinus,
FiMoreHorizontal,
FiPlus,
FiShare2,
FiTrash,
FiX,
} from "react-icons/fi";
import Link from "next/link";
import {
addAssistantToList,
removeAssistantFromList,
updateUserAssistantList,
} from "@/lib/assistants/updateAssistantPreferences";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { DefaultPopover } from "@/components/popover/DefaultPopover";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { AssistantsPageTitle } from "../AssistantsPageTitle";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { AssistantSharingModal } from "./AssistantSharingModal";
import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { DragHandle } from "@/components/table/DragHandle";
import {
deletePersona,
togglePersonaPublicStatus,
} from "@/app/admin/assistants/lib";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantModal";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { useAssistants } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
function DraggableAssistantListItem({ ...props }: any) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.assistant.id.toString() });
const style = {
transform: transform
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
: undefined,
transition,
opacity: isDragging ? 0.9 : 1,
zIndex: isDragging ? 1000 : "auto",
};
return (
<div ref={setNodeRef} style={style} className="flex mt-2 items-center">
<div {...attributes} {...listeners} className="mr-2 cursor-grab">
<DragHandle />
</div>
<div className="flex-grow">
<AssistantListItem isDragging={isDragging} {...props} />
</div>
</div>
);
}
function AssistantListItem({
assistant,
user,
allUsers,
isVisible,
setPopup,
deleteAssistant,
shareAssistant,
isDragging,
onlyAssistant,
}: {
assistant: Persona;
user: User | null;
allUsers: MinimalUserSnapshot[];
isVisible: boolean;
deleteAssistant: Dispatch<SetStateAction<Persona | null>>;
shareAssistant: Dispatch<SetStateAction<Persona | null>>;
setPopup: (popupSpec: PopupSpec | null) => void;
isDragging?: boolean;
onlyAssistant: boolean;
}) {
const { refreshUser } = useUser();
const router = useRouter();
const [showSharingModal, setShowSharingModal] = useState(false);
const isEnterpriseEnabled = usePaidEnterpriseFeaturesEnabled();
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
const { isAdmin } = useUser();
return (
<>
<AssistantSharingModal
assistant={assistant}
user={user}
allUsers={allUsers}
onClose={() => {
setShowSharingModal(false);
router.refresh();
}}
show={showSharingModal}
/>
<div
className={`rounded-lg px-4 py-6 transition-all duration-900 hover:bg-background-125 ${
isDragging && "bg-background-125"
}`}
>
<div className="flex justify-between items-center">
<AssistantIcon assistant={assistant} />
<h2 className="ml-6 w-fit flex-grow space-y-3 text-start flex text-xl font-semibold line-clamp-2 text-gray-800">
{assistant.name}
</h2>
<div className="flex flex-none items-center space-x-4">
<div className="flex mr-20 flex-wrap items-center gap-x-4">
{assistant.tools.length > 0 && (
<p className="text-base flex w-fit text-subtle">
{assistant.tools.length} tool
{assistant.tools.length > 1 && "s"}
</p>
)}
<AssistantSharedStatusDisplay
size="md"
assistant={assistant}
user={user}
/>
</div>
{isOwnedByUser ? (
<Link
href={`/assistants/edit/${assistant.id}`}
className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200"
title="Edit assistant"
>
<FiEdit2 size={20} className="text-text-900" />
</Link>
) : (
<CustomTooltip
showTick
content="You don't have permission to edit this assistant"
>
<div className="p-2 cursor-not-allowed opacity-50 rounded-full hover:bg-gray-100 transition-colors duration-200">
<FiEdit2 size={20} className="text-text-900" />
</div>
</CustomTooltip>
)}
<DefaultPopover
content={
<div className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200 cursor-pointer">
<FiMoreHorizontal size={20} className="text-text-900" />
</div>
}
side="bottom"
align="end"
sideOffset={5}
>
{[
isVisible ? (
<button
key="remove"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={async () => {
if (onlyAssistant) {
setPopup({
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
type: "error",
});
return;
}
const success = await removeAssistantFromList(
assistant.id
);
if (success) {
setPopup({
message: `"${assistant.name}" has been removed from your list.`,
type: "success",
});
await refreshUser();
} else {
setPopup({
message: `"${assistant.name}" could not be removed from your list.`,
type: "error",
});
}
}}
>
<FiX size={18} className="text-text-800" />{" "}
{isOwnedByUser ? "Hide" : "Remove"}
</button>
) : (
<button
key="add"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={async () => {
const success = await addAssistantToList(assistant.id);
if (success) {
setPopup({
message: `"${assistant.name}" has been added to your list.`,
type: "success",
});
await refreshUser();
} else {
setPopup({
message: `"${assistant.name}" could not be added to your list.`,
type: "error",
});
}
}}
>
<FiPlus size={18} className="text-text-800" /> Add
</button>
),
(isOwnedByUser || isAdmin) && isEnterpriseEnabled ? (
<button
key="view-stats"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={() =>
router.push(`/assistants/stats/${assistant.id}`)
}
>
<FiBarChart size={18} /> View Stats
</button>
) : null,
isOwnedByUser ? (
<button
key="delete"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left text-red-600"
onClick={() => deleteAssistant(assistant)}
>
<FiTrash size={18} /> Delete
</button>
) : null,
isOwnedByUser ? (
<button
key="visibility"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={() => shareAssistant(assistant)}
>
{assistant.is_public ? (
<FiMinus size={18} className="text-text-800" />
) : (
<FiPlus size={18} className="text-text-800" />
)}{" "}
Make {assistant.is_public ? "Private" : "Public"}
</button>
) : null,
!assistant.is_public ? (
<button
key="share"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={(e) => {
setShowSharingModal(true);
}}
>
<FiShare2 size={18} className="text-text-800" /> Share
</button>
) : null,
]}
</DefaultPopover>
</div>
{/* )} */}
</div>
</div>
</>
);
}
export function AssistantsList() {
const {
assistants,
ownedButHiddenAssistants,
finalAssistants,
refreshAssistants,
} = useAssistants();
const [currentlyVisibleAssistants, setCurrentlyVisibleAssistants] =
useState(finalAssistants);
useEffect(() => {
setCurrentlyVisibleAssistants(finalAssistants);
}, [finalAssistants]);
const allAssistantIds = assistants.map((assistant) =>
assistant.id.toString()
);
const [deletingPersona, setDeletingPersona] = useState<Persona | null>(null);
const [makePublicPersona, setMakePublicPersona] = useState<Persona | null>(
null
);
const { refreshUser, user } = useUser();
const { popup, setPopup } = usePopup();
const router = useRouter();
const { data: users } = useSWR<MinimalUserSnapshot[]>(
"/api/users",
errorHandlingFetcher
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
async function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = currentlyVisibleAssistants.findIndex(
(item) => item.id.toString() === active.id
);
const newIndex = currentlyVisibleAssistants.findIndex(
(item) => item.id.toString() === over.id
);
const updatedAssistants = arrayMove(
currentlyVisibleAssistants,
oldIndex,
newIndex
);
setCurrentlyVisibleAssistants(updatedAssistants);
await updateUserAssistantList(updatedAssistants.map((a) => a.id));
}
}
return (
<>
{popup}
{deletingPersona && (
<DeleteEntityModal
entityType="Assistant"
entityName={deletingPersona.name}
onClose={() => setDeletingPersona(null)}
onSubmit={async () => {
const success = await deletePersona(deletingPersona.id);
if (success) {
setPopup({
message: `"${deletingPersona.name}" has been deleted.`,
type: "success",
});
await refreshUser();
} else {
setPopup({
message: `"${deletingPersona.name}" could not be deleted.`,
type: "error",
});
}
setDeletingPersona(null);
}}
/>
)}
{makePublicPersona && (
<MakePublicAssistantModal
isPublic={makePublicPersona.is_public}
onClose={() => setMakePublicPersona(null)}
onShare={async (newPublicStatus: boolean) => {
await togglePersonaPublicStatus(
makePublicPersona.id,
newPublicStatus
);
await refreshAssistants();
}}
/>
)}
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<AssistantsPageTitle>Your Assistants</AssistantsPageTitle>
<div className="grid grid-cols-2 gap-4 mt-4 mb-8">
<Button
variant="default"
className="p-6 text-base"
onClick={() => router.push("/assistants/new")}
icon={FiPlus}
>
Create New Assistant
</Button>
<Button
onClick={() => router.push("/assistants/gallery")}
variant="outline"
className="text-base py-6"
icon={FiList}
>
Assistant Gallery
</Button>
</div>
<h2 className="text-2xl font-semibold mb-2 text-text-900">
Active Assistants
</h2>
<h3 className="text-lg text-text-500">
The order the assistants appear below will be the order they appear in
the Assistants dropdown. The first assistant listed will be your
default assistant when you start a new chat. Drag and drop to reorder.
</h3>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={currentlyVisibleAssistants.map((a) => a.id.toString())}
strategy={verticalListSortingStrategy}
>
<div className="w-full items-center py-4">
{currentlyVisibleAssistants.map((assistant, index) => (
<DraggableAssistantListItem
onlyAssistant={currentlyVisibleAssistants.length === 1}
deleteAssistant={setDeletingPersona}
shareAssistant={setMakePublicPersona}
key={assistant.id}
assistant={assistant}
user={user}
allAssistantIds={allAssistantIds}
allUsers={users || []}
isVisible
setPopup={setPopup}
/>
))}
</div>
</SortableContext>
</DndContext>
{ownedButHiddenAssistants.length > 0 && (
<>
<Separator />
<h3 className="text-xl font-bold mb-4">Your Hidden Assistants</h3>
<h3 className="text-lg text-text-500">
Assistants you&apos;ve created that aren&apos;t currently visible
in the Assistants selector.
</h3>
<div className="w-full p-4">
{ownedButHiddenAssistants.map((assistant, index) => (
<AssistantListItem
onlyAssistant={currentlyVisibleAssistants.length === 1}
deleteAssistant={setDeletingPersona}
shareAssistant={setMakePublicPersona}
key={assistant.id}
assistant={assistant}
user={user}
allUsers={users || []}
isVisible={false}
setPopup={setPopup}
/>
))}
</div>
</>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,31 @@
import React from "react";
import { FiTrash } from "react-icons/fi";
import { Button } from "@/components/ui/button";
interface DeleteAssistantPopoverProps {
entityName: string;
onClose: () => void;
onSubmit: () => void;
}
export function DeleteAssistantPopover({
entityName,
onClose,
onSubmit,
}: DeleteAssistantPopoverProps) {
return (
<div className="w-full">
<p className="text-sm mb-3">
Are you sure you want to delete assistant <b>{entityName}</b>?
</p>
<div className="flex justify-center gap-2">
<Button variant="secondary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button variant="destructive" size="sm" onClick={onSubmit}>
Delete
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,69 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
interface MakePublicAssistantPopoverProps {
isPublic: boolean;
onShare: (shared: boolean) => void;
onClose: () => void;
}
export function MakePublicAssistantPopover({
isPublic,
onShare,
onClose,
}: MakePublicAssistantPopoverProps) {
return (
<div className="p-4 space-y-4">
<h2 className="text-lg font-semibold">
{isPublic ? "Public Assistant" : "Make Assistant Public"}
</h2>
<p className="text-sm">
This assistant is currently{" "}
<span className="font-semibold">{isPublic ? "public" : "private"}</span>
.
{isPublic
? " Anyone can currently access this assistant."
: " Only you can access this assistant."}
</p>
<Separator />
{isPublic ? (
<div className="space-y-4">
<p className="text-sm">
To restrict access to this assistant, you can make it private again.
</p>
<Button
onClick={async () => {
await onShare(false);
onClose();
}}
size="sm"
variant="destructive"
>
Make Assistant Private
</Button>
</div>
) : (
<div className="space-y-4">
<p className="text-sm">
Making this assistant public will allow anyone with the link to view
and use it. Ensure that all content and capabilities of the
assistant are safe to share.
</p>
<Button
onClick={async () => {
await onShare(true);
onClose();
}}
size="sm"
>
Make Assistant Public
</Button>
</div>
)}
</div>
);
}

View File

@@ -1,15 +0,0 @@
"use client";
import { AssistantsList } from "./AssistantsList";
import SidebarWrapper from "../SidebarWrapper";
export default function WrappedAssistantsMine({
initiallyToggled,
}: {
initiallyToggled: boolean;
}) {
return (
<SidebarWrapper page="chat" initiallyToggled={initiallyToggled}>
<AssistantsList />
</SidebarWrapper>
);
}

View File

@@ -1,64 +0,0 @@
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { unstable_noStore as noStore } from "next/cache";
import { redirect } from "next/navigation";
import WrappedAssistantsMine from "./WrappedAssistantsMine";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { cookies } from "next/headers";
import { ChatProvider } from "@/components/context/ChatContext";
export default async function GalleryPage(props: {
searchParams: Promise<{ [key: string]: string }>;
}) {
noStore();
const requestCookies = await cookies();
const searchParams = await props.searchParams;
const data = await fetchChatData(searchParams);
if ("redirect" in data) {
redirect(data.redirect);
}
const {
user,
chatSessions,
folders,
openedFolders,
toggleSidebar,
shouldShowWelcomeModal,
availableSources,
ccPairs,
documentSets,
tags,
llmProviders,
defaultAssistantId,
} = data;
return (
<ChatProvider
value={{
chatSessions,
availableSources,
ccPairs,
documentSets,
tags,
availableDocumentSets: documentSets,
availableTags: tags,
llmProviders,
folders,
openedFolders,
shouldShowWelcomeModal,
defaultAssistantId,
}}
>
{shouldShowWelcomeModal && (
<WelcomeModal user={user} requestCookies={requestCookies} />
)}
<InstantSSRAutoRefresh />
<WrappedAssistantsMine initiallyToggled={toggleSidebar} />
</ChatProvider>
);
}

View File

@@ -5,6 +5,7 @@ import { SuccessfulPersonaUpdateRedirectType } from "@/app/admin/assistants/enum
import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { LargeBackButton } from "../LargeBackButton";
import { BackButton } from "@/components/BackButton";
export default async function Page() {
const [values, error] = await fetchAssistantEditorInfoSS();
@@ -18,10 +19,10 @@ export default async function Page() {
);
} else {
body = (
<div className="w-full my-16">
<div className="w-full py-8">
<div className="px-32">
<div className="mx-auto container">
<CardSection>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
defaultPublic={false}
@@ -35,21 +36,5 @@ export default async function Page() {
);
}
return (
<div>
<HeaderWrapper>
<div className="h-full flex flex-col">
<div className="flex my-auto">
<LargeBackButton />
<h1 className="flex text-xl text-strong font-bold my-auto">
New Assistant
</h1>
</div>
</div>
</HeaderWrapper>
{body}
</div>
);
return <div>{body}</div>;
}

View File

@@ -0,0 +1,106 @@
"use client";
import { AuthTypeMetadata } from "@/lib/userSS";
import { LoginText } from "./LoginText";
import Link from "next/link";
import { SignInButton } from "./SignInButton";
import { EmailPasswordForm } from "./EmailPasswordForm";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import Title from "@/components/ui/title";
import { useSendAuthRequiredMessage } from "@/lib/extension/utils";
export default function LoginPage({
authUrl,
authTypeMetadata,
nextUrl,
searchParams,
showPageRedirect,
}: {
authUrl: string | null;
authTypeMetadata: AuthTypeMetadata | null;
nextUrl: string | null;
searchParams:
| {
[key: string]: string | string[] | undefined;
}
| undefined;
showPageRedirect?: boolean;
}) {
useSendAuthRequiredMessage();
return (
<div className="flex flex-col w-full justify-center">
{authUrl && authTypeMetadata && (
<>
<h2 className="text-center text-xl text-strong font-bold">
<LoginText />
</h2>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="mt-4 w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-gray-300"></div>
<span className="px-4 text-gray-500">or</span>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
<div className="flex mt-4 justify-between">
<Link
href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : ""
}`}
className="text-link font-medium"
>
Create an account
</Link>
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<Link
href="/auth/forgot-password"
className="text-link font-medium"
>
Reset Password
</Link>
)}
</div>
</div>
)}
{authTypeMetadata?.authType === "basic" && (
<>
<div className="flex">
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
<LoginText />
</Title>
</div>
<EmailPasswordForm nextUrl={nextUrl} />
<div className="flex flex-col gap-y-2 items-center"></div>
</>
)}
{showPageRedirect && (
<p className="text-center mt-4">
Don&apos;t have an account?{" "}
<span
onClick={() => {
if (typeof window !== "undefined" && window.top) {
window.top.location.href = "/auth/signup";
} else {
window.location.href = "/auth/signup";
}
}}
className="text-link font-medium cursor-pointer"
>
Create an account
</span>
</p>
)}
</div>
);
}

View File

@@ -46,7 +46,7 @@ export function SignInButton({
return (
<a
className="mx-auto mt-6 py-3 w-full text-text-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
className="mx-auto mb-4 mt-6 py-3 w-full text-text-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
href={finalAuthorizeUrl}
>
{button}

View File

@@ -7,18 +7,8 @@ import {
AuthTypeMetadata,
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import { SignInButton } from "./SignInButton";
import { EmailPasswordForm } from "./EmailPasswordForm";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
import Link from "next/link";
import { LoginText } from "./LoginText";
import { getSecondsUntilExpiration } from "@/lib/time";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useContext } from "react";
import LoginPage from "./LoginPage";
const Page = async (props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -49,13 +39,7 @@ const Page = async (props: {
}
// if user is already logged in, take them to the main app page
const secondsTillExpiration = getSecondsUntilExpiration(currentUser);
if (
currentUser &&
currentUser.is_active &&
!currentUser.is_anonymous_user &&
(secondsTillExpiration === null || secondsTillExpiration > 0)
) {
if (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) {
return redirect("/auth/waiting-on-verification");
}
@@ -83,55 +67,12 @@ const Page = async (props: {
<HealthCheckBanner />
</div>
<div className="flex flex-col w-full justify-center">
{authUrl && authTypeMetadata && (
<>
<h2 className="text-center text-xl text-strong font-bold">
<LoginText />
</h2>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="mt-4 w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-gray-300"></div>
<span className="px-4 text-gray-500">or</span>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
<div className="flex mt-4 justify-between">
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<Link
href="/auth/forgot-password"
className="text-link font-medium"
>
Reset Password
</Link>
)}
</div>
</div>
)}
{authTypeMetadata?.authType === "basic" && (
<>
<div className="flex">
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
<LoginText />
</Title>
</div>
<EmailPasswordForm nextUrl={nextUrl} />
<div className="flex flex-col gap-y-2 items-center"></div>
</>
)}
</div>
<LoginPage
authUrl={authUrl}
authTypeMetadata={authTypeMetadata}
nextUrl={nextUrl!}
searchParams={searchParams}
/>
</AuthFlowContainer>
</div>
);

View File

@@ -2,17 +2,18 @@
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useContext, useState, useRef, useLayoutEffect } from "react";
import { Popover } from "@/components/popover/Popover";
import { ChevronDownIcon } from "@/components/icons/icons";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
export function ChatBanner() {
const settings = useContext(SettingsContext);
const [isOverflowing, setIsOverflowing] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const fullContentRef = useRef<HTMLDivElement>(null);
// Check for text overflow
useLayoutEffect(() => {
const checkOverflow = () => {
if (contentRef.current && fullContentRef.current) {
@@ -31,93 +32,92 @@ export function ChatBanner() {
return () => window.removeEventListener("resize", checkOverflow);
}, []);
// Bail out if no custom header content
if (!settings?.enterpriseSettings?.custom_header_content) {
return null;
}
const handleMouseEnter = () => setIsExpanded(true);
const handleMouseLeave = () => setIsExpanded(false);
return (
<div
className={`
px-2
z-[39]
py-1.5
text-wrap
z-[39]
w-full
mx-auto
relative
cursor-default
shadow-sm
rounded
border-l-8 border-l-400
bg-background
border-r-4 border-r-200
border-border
border
flex`}
border-border
border-l-8 border-l-400
border-r-4 border-r-200
bg-background-sidebar
transition-all duration-300 ease-in-out
${isExpanded ? "shadow-md bg-background-100" : ""}
`}
onMouseLeave={handleMouseLeave}
aria-expanded={isExpanded}
role="region"
>
<div className="text-emphasis text-sm w-full">
<div className="relative">
<div className={`flex justify-center w-full overflow-hidden pr-8`}>
<div
ref={contentRef}
className={`overflow-hidden ${
settings.enterpriseSettings.two_lines_for_chat_header
? "line-clamp-2"
: "line-clamp-1"
} text-center max-w-full`}
>
{/* Padding for consistent spacing */}
<div className="relative p-2">
{/* Collapsible container */}
<div
className={`
overflow-hidden
transition-all duration-300 ease-in-out
${
isExpanded
? "max-h-[1000px]"
: settings.enterpriseSettings.two_lines_for_chat_header
? "max-h-[3em]" // ~3 lines
: "max-h-[1.5em]" // ~1.5 lines
}
`}
>
{/* Visible content container */}
<div ref={contentRef} className="text-center max-w-full">
<MinimalMarkdown
className="prose text-sm max-w-full"
// Ensure text can wrap to multiple lines
className="prose text-left text-sm max-w-full whitespace-normal break-words"
content={settings.enterpriseSettings.custom_header_content}
/>
</div>
</div>
<div className="absolute top-0 left-0 invisible flex justify-center max-w-full">
{/* Invisible element to measure overflow */}
<div className="absolute top-0 left-0 invisible">
<div
ref={fullContentRef}
className={`overflow-hidden invisible ${
settings.enterpriseSettings.two_lines_for_chat_header
? "line-clamp-2"
: "line-clamp-1"
} text-center max-w-full`}
className="overflow-hidden invisible text-center max-w-full"
>
<MinimalMarkdown
className="prose text-sm max-w-full"
// Same wrapping behavior as visible content
className="prose text-sm max-w-full whitespace-normal break-words"
content={settings.enterpriseSettings.custom_header_content}
/>
</div>
</div>
<div className="absolute bottom-0 right-0">
{isOverflowing && (
<Popover
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
content={
<button
onClick={() => setIsPopoverOpen(true)}
className="cursor-poiner bg-background-100 p-1 rounded-full"
>
<ChevronDownIcon className="h-4 w-4 text-emphasis" />
</button>
}
popover={
<div className="bg-background-100 p-4 rounded shadow-lg mobile:max-w-xs desktop:max-w-md">
<p className="text-lg font-bold">Banner Content</p>
<MinimalMarkdown
className="max-h-96 overflow-y-auto"
content={
settings.enterpriseSettings.custom_header_content
}
/>
</div>
}
side="bottom"
align="end"
/>
)}
</div>
{/* Chevron button if content is truncated */}
</div>
</div>
<div className="absolute -top-1 right-0">
{isOverflowing && !isExpanded && (
<button
onMouseEnter={handleMouseEnter}
className="cursor-pointer bg-background-100 p-.5 rounded-full transition-opacity duration-300 ease-in-out"
aria-label="Expand banner content"
onClick={() => setIsExpanded(true)}
>
<ChevronDownIcon className="h-3 w-3 text-emphasis" />
</button>
)}
</div>
</div>
);
}

View File

@@ -1,42 +1,22 @@
import { Persona } from "../admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { useState } from "react";
import { DisplayAssistantCard } from "@/components/assistants/AssistantDescriptionCard";
import { Persona } from "../admin/assistants/interfaces";
import { OnyxIcon } from "@/components/icons/icons";
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
const [hoveredAssistant, setHoveredAssistant] = useState(false);
return (
<div className="flex flex-col items-center gap-6">
<div className="relative flex w-fit mx-auto justify-center">
<div className="absolute z-10 -left-20 top-1/2 -translate-y-1/2">
<div className="relative">
<div
onMouseEnter={() => setHoveredAssistant(true)}
onMouseLeave={() => setHoveredAssistant(false)}
className="p-4 scale-[.7] cursor-pointer border-dashed rounded-full flex border border-gray-300 border-2 border-dashed"
>
<AssistantIcon
disableToolip
size="large"
assistant={selectedPersona}
/>
</div>
<div className="absolute right-full mr-1 w-[300px] top-0">
{hoveredAssistant && (
<DisplayAssistantCard selectedPersona={selectedPersona} />
)}
</div>
</div>
<div className="relative flex flex-col gap-y-4 w-fit mx-auto justify-center">
<div className="absolute z-10 items-center flex -left-12 top-1/2 -translate-y-1/2">
<AssistantIcon size={36} assistant={selectedPersona} />
</div>
<div className="text-2xl text-black font-semibold text-center">
<div className="text-4xl text-text font-normal text-center">
{selectedPersona.name}
</div>
</div>
<p className="text-base text-black font-normal text-center">
<div className="self-stretch text-center text-text-darker text-xl font-[350] leading-normal">
{selectedPersona.description}
</p>
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { redirect, useRouter, useSearchParams } from "next/navigation";
import {
BackendChatSession,
BackendMessage,
@@ -28,7 +28,6 @@ import {
checkAnyAssistantHasSearch,
createChatSession,
deleteAllChatSessions,
deleteChatSession,
getCitedDocumentsFromMessage,
getHumanAndAIMessageFromMessageNumber,
getLastSuccessfulMessageId,
@@ -47,20 +46,19 @@ import {
import {
Dispatch,
SetStateAction,
use,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
useMemo,
} from "react";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SEARCH_PARAM_NAMES, shouldSubmitOnLoad } from "./searchParams";
import { useDocumentSelection } from "./useDocumentSelection";
import { LlmOverride, useFilters, useLlmOverride } from "@/lib/hooks";
import { computeAvailableFilters } from "@/lib/filters";
import { ChatState, FeedbackType, RegenerationState } from "./types";
import { ChatFilters } from "./documentSidebar/ChatFilters";
import { DocumentResults } from "./documentSidebar/DocumentResults";
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
import { FeedbackModal } from "./modal/FeedbackModal";
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
@@ -94,7 +92,7 @@ import FunctionalHeader from "@/components/chat_search/Header";
import { useSidebarVisibility } from "@/components/chat_search/hooks";
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
import FixedLogo from "./shared_chat_search/FixedLogo";
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
import { DeleteEntityModal } from "../../components/modals/DeleteEntityModal";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
@@ -105,12 +103,16 @@ import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import BlurBackground from "./shared_chat_search/BlurBackground";
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
import { useAssistants } from "@/components/context/AssistantsContext";
import { Separator } from "@/components/ui/separator";
import AssistantBanner from "../../components/assistants/AssistantBanner";
import TextView from "@/components/chat_search/TextView";
import AssistantSelector from "@/components/chat_search/AssistantSelector";
import { Modal } from "@/components/Modal";
import { createPostponedAbortSignal } from "next/dist/server/app-render/dynamic-rendering";
import { useSendMessageToParent } from "@/lib/extension/utils";
import {
CHROME_MESSAGE,
SUBMIT_MESSAGE_TYPES,
} from "@/lib/extension/constants";
import AssistantModal from "../assistants/mine/AssistantModal";
import { getSourceMetadata } from "@/lib/sources";
import { UserSettingsModal } from "./modal/UserSettingsModal";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -120,10 +122,12 @@ export function ChatPage({
toggle,
documentSidebarInitialWidth,
toggledSidebar,
firstMessage,
}: {
toggle: (toggled?: boolean) => void;
documentSidebarInitialWidth?: number;
toggledSidebar: boolean;
firstMessage?: string;
}) {
const router = useRouter();
const searchParams = useSearchParams();
@@ -136,10 +140,15 @@ export function ChatPage({
llmProviders,
folders,
openedFolders,
defaultAssistantId,
shouldShowWelcomeModal,
refreshChatSessions,
} = useChatContext();
const defaultAssistantIdRaw = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
const defaultAssistantId = defaultAssistantIdRaw
? parseInt(defaultAssistantIdRaw)
: undefined;
function useScreenSize() {
const [screenSize, setScreenSize] = useState({
width: typeof window !== "undefined" ? window.innerWidth : 0,
@@ -179,7 +188,6 @@ export function ChatPage({
const enterpriseSettings = settings?.enterpriseSettings;
const [documentSidebarToggled, setDocumentSidebarToggled] = useState(false);
const [filtersToggled, setFiltersToggled] = useState(false);
const [userSettingsToggled, setUserSettingsToggled] = useState(false);
@@ -192,13 +200,7 @@ export function ChatPage({
const { user, isAdmin } = useUser();
const slackChatId = searchParams.get("slackChatId");
const existingChatIdRaw = searchParams.get("chatId");
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
);
const modelVersionFromSearchParams = searchParams.get(
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
);
const [showHistorySidebar, setShowHistorySidebar] = useState(false); // State to track if sidebar is open
useEffect(() => {
@@ -210,24 +212,33 @@ export function ChatPage({
toggle(false);
}
}, [user]);
// Effect to handle sendOnLoad
useEffect(() => {
if (sendOnLoad) {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD);
// Update the URL without the send-on-load parameter
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
const processSearchParamsAndSubmitMessage = (searchParamsString: string) => {
const newSearchParams = new URLSearchParams(searchParamsString);
const message = newSearchParams.get("user-prompt");
// Update our local state to reflect the change
setSendOnLoad(null);
filterManager.buildFiltersFromQueryString(
newSearchParams.toString(),
availableSources,
documentSets.map((ds) => ds.name),
tags
);
// If there's a message, submit it
if (message) {
onSubmit({ messageOverride: message });
}
const fileDescriptorString = newSearchParams.get(SEARCH_PARAM_NAMES.FILES);
const overrideFileDescriptors: FileDescriptor[] = fileDescriptorString
? JSON.parse(decodeURIComponent(fileDescriptorString))
: [];
newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD);
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
// If there's a message, submit it
if (message) {
setSubmittedMessage(message);
onSubmit({ messageOverride: message, overrideFileDescriptors });
}
}, [sendOnLoad, searchParams, router]);
};
const existingChatSessionId = existingChatIdRaw ? existingChatIdRaw : null;
@@ -284,9 +295,8 @@ export function ChatPage({
const llmOverrideManager = useLlmOverride(
llmProviders,
modelVersionFromSearchParams || (user?.preferences.default_model ?? null),
selectedChatSession,
defaultTemperature
user?.preferences.default_model,
selectedChatSession
);
const [alternativeAssistant, setAlternativeAssistant] =
@@ -312,14 +322,8 @@ export function ChatPage({
const noAssistants = liveAssistant == null || liveAssistant == undefined;
const availableSources = ccPairs.map((ccPair) => ccPair.source);
const [finalAvailableSources, finalAvailableDocumentSets] =
computeAvailableFilters({
selectedPersona: availableAssistants.find(
(assistant) => assistant.id === liveAssistant?.id
),
availableSources: availableSources,
availableDocumentSets: documentSets,
});
const uniqueSources = Array.from(new Set(availableSources));
const sources = uniqueSources.map((source) => getSourceMetadata(source));
// always set the model override for the chat session, when an assistant, llm provider, or user preference exists
useEffect(() => {
@@ -399,8 +403,6 @@ export function ChatPage({
setIsReady(true);
}, []);
// this is triggered every time the user switches which chat
// session they are using
useEffect(() => {
const priorChatSessionId = chatSessionIdRef.current;
const loadedSessionId = loadedIdSessionRef.current;
@@ -456,7 +458,6 @@ export function ChatPage({
}
return;
}
setIsReady(true);
const shouldScrollToBottom =
visibleRange.get(existingChatSessionId) === undefined ||
visibleRange.get(existingChatSessionId)?.end == 0;
@@ -535,7 +536,7 @@ export function ChatPage({
initialSessionFetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingChatSessionId]);
}, [existingChatSessionId, searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID)]);
const [message, setMessage] = useState(
searchParams.get(SEARCH_PARAM_NAMES.USER_PROMPT) || ""
@@ -651,10 +652,10 @@ export function ChatPage({
currentMessageMap(completeMessageDetail)
);
const [submittedMessage, setSubmittedMessage] = useState("");
const [submittedMessage, setSubmittedMessage] = useState(firstMessage || "");
const [chatState, setChatState] = useState<Map<string | null, ChatState>>(
new Map([[chatSessionIdRef.current, "input"]])
new Map([[chatSessionIdRef.current, firstMessage ? "loading" : "input"]])
);
const [regenerationState, setRegenerationState] = useState<
@@ -798,6 +799,19 @@ export function ChatPage({
}
}, [defaultAssistantId, availableAssistants, messageHistory.length]);
useEffect(() => {
if (
submittedMessage &&
currentSessionChatState === "loading" &&
messageHistory.length == 0
) {
window.parent.postMessage(
{ type: CHROME_MESSAGE.LOAD_NEW_CHAT_PAGE },
"*"
);
}
}, [submittedMessage, currentSessionChatState]);
const [
selectedDocuments,
toggleDocumentSelection,
@@ -990,19 +1004,38 @@ export function ChatPage({
if (
!personaIncludesRetrieval &&
(!selectedDocuments || selectedDocuments.length === 0) &&
documentSidebarToggled &&
!filtersToggled
documentSidebarToggled
) {
setDocumentSidebarToggled(false);
}
}, [chatSessionIdRef.current]);
const loadNewPageLogic = (event: MessageEvent) => {
if (event.data.type === SUBMIT_MESSAGE_TYPES.PAGE_CHANGE) {
try {
const url = new URL(event.data.href);
processSearchParamsAndSubmitMessage(url.searchParams.toString());
} catch (error) {
console.error("Error parsing URL:", error);
}
}
};
// Equivalent to `loadNewPageLogic`
useEffect(() => {
adjustDocumentSidebarWidth(); // Adjust the width on initial render
window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener
if (searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)) {
processSearchParamsAndSubmitMessage(searchParams.toString());
}
}, [searchParams, router]);
useEffect(() => {
adjustDocumentSidebarWidth();
window.addEventListener("resize", adjustDocumentSidebarWidth);
window.addEventListener("message", loadNewPageLogic);
return () => {
window.removeEventListener("resize", adjustDocumentSidebarWidth); // Cleanup the event listener
window.removeEventListener("message", loadNewPageLogic);
window.removeEventListener("resize", adjustDocumentSidebarWidth);
};
}, []);
@@ -1078,6 +1111,7 @@ export function ChatPage({
alternativeAssistantOverride = null,
modelOverRide,
regenerationRequest,
overrideFileDescriptors,
}: {
messageIdToResend?: number;
messageOverride?: string;
@@ -1087,6 +1121,7 @@ export function ChatPage({
alternativeAssistantOverride?: Persona | null;
modelOverRide?: LlmOverride;
regenerationRequest?: RegenerationRequest | null;
overrideFileDescriptors?: FileDescriptor[];
} = {}) => {
let frozenSessionId = currentSessionId();
updateCanContinue(false, frozenSessionId);
@@ -1113,6 +1148,7 @@ export function ChatPage({
let currChatSessionId: string;
const isNewSession = chatSessionIdRef.current === null;
const searchParamBasedChatSessionName =
searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null;
@@ -1228,7 +1264,7 @@ export function ChatPage({
signal: controller.signal, // Add this line
message: currMessage,
alternateAssistantId: currentAssistantId,
fileDescriptors: currentMessageFiles,
fileDescriptors: overrideFileDescriptors || currentMessageFiles,
parentMessageId:
regenerationRequest?.parentMessage.messageId ||
lastSuccessfulMessageId,
@@ -1529,6 +1565,7 @@ export function ChatPage({
setSelectedMessageForDocDisplay(finalMessage.message_id);
}
setAlternativeGeneratingAssistant(null);
setSubmittedMessage("");
};
const onFeedback = async (
@@ -1815,28 +1852,27 @@ export function ChatPage({
end: 0,
mostVisibleMessageId: null,
};
useSendMessageToParent();
useEffect(() => {
if (noAssistants) {
return;
if (liveAssistant) {
const hasSearchTool = liveAssistant.tools.some(
(tool) => tool.in_code_tool_id === "SearchTool"
);
setRetrievalEnabled(hasSearchTool);
if (!hasSearchTool) {
filterManager.clearFilters();
}
}
const includes = checkAnyAssistantHasSearch(
messageHistory,
availableAssistants,
liveAssistant
);
setRetrievalEnabled(includes);
}, [messageHistory, availableAssistants, liveAssistant]);
}, [liveAssistant]);
const [retrievalEnabled, setRetrievalEnabled] = useState(() => {
if (noAssistants) {
return false;
if (liveAssistant) {
return liveAssistant.tools.some(
(tool) => tool.in_code_tool_id === "SearchTool"
);
}
return checkAnyAssistantHasSearch(
messageHistory,
availableAssistants,
liveAssistant
);
return false;
});
useEffect(() => {
@@ -1889,6 +1925,7 @@ export function ChatPage({
handleSlackChatRedirect();
}, [searchParams, router]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
@@ -1909,34 +1946,17 @@ export function ChatPage({
}, [router]);
const [sharedChatSession, setSharedChatSession] =
useState<ChatSession | null>();
const [deletingChatSession, setDeletingChatSession] =
useState<ChatSession | null>();
const showDeleteModal = (chatSession: ChatSession) => {
setDeletingChatSession(chatSession);
};
const showShareModal = (chatSession: ChatSession) => {
setSharedChatSession(chatSession);
};
const [showAssistantsModal, setShowAssistantsModal] = useState(false);
const toggleDocumentSidebar = () => {
if (!documentSidebarToggled) {
setFiltersToggled(false);
setDocumentSidebarToggled(true);
} else if (!filtersToggled) {
setDocumentSidebarToggled(false);
} else {
setFiltersToggled(false);
}
};
const toggleFilters = () => {
if (!documentSidebarToggled) {
setFiltersToggled(true);
setDocumentSidebarToggled(true);
} else if (filtersToggled) {
setDocumentSidebarToggled(false);
} else {
setFiltersToggled(true);
}
};
@@ -1957,6 +1977,10 @@ export function ChatPage({
});
};
}
if (!user) {
redirect("/auth/login");
}
if (noAssistants)
return (
<>
@@ -2025,7 +2049,7 @@ export function ChatPage({
)}
{(settingsToggled || userSettingsToggled) && (
<SetDefaultModelModal
<UserSettingsModal
setPopup={setPopup}
setLlmOverride={llmOverrideManager.setGlobalDefault}
defaultModel={user?.preferences.default_model!}
@@ -2039,16 +2063,15 @@ export function ChatPage({
{retrievalEnabled && documentSidebarToggled && settings?.isMobile && (
<div className="md:hidden">
<Modal noPadding noScroll>
<ChatFilters
<Modal
onOutsideClick={() => setDocumentSidebarToggled(false)}
noPadding
noScroll
>
<DocumentResults
setPresentingDocument={setPresentingDocument}
modal={true}
filterManager={filterManager}
ccPairs={ccPairs}
tags={tags}
documentSets={documentSets}
ref={innerSidebarElementRef}
showFilters={filtersToggled}
closeSidebar={() => {
setDocumentSidebarToggled(false);
}}
@@ -2065,28 +2088,6 @@ export function ChatPage({
</div>
)}
{deletingChatSession && (
<DeleteEntityModal
entityType="chat"
entityName={deletingChatSession.name.slice(0, 30)}
onClose={() => setDeletingChatSession(null)}
onSubmit={async () => {
const response = await deleteChatSession(deletingChatSession.id);
if (response.ok) {
setDeletingChatSession(null);
// go back to the main page
if (deletingChatSession.id === chatSessionIdRef.current) {
router.push("/chat");
}
} else {
const responseJson = await response.json();
setPopup({ message: responseJson.detail, type: "error" });
}
refreshChatSessions();
}}
/>
)}
{presentingDocument && (
<TextView
presentingDocument={presentingDocument}
@@ -2130,6 +2131,10 @@ export function ChatPage({
/>
)}
{showAssistantsModal && (
<AssistantModal hideModal={() => setShowAssistantsModal(false)} />
)}
<div className="fixed inset-0 flex flex-col text-default">
<div className="h-[100dvh] overflow-y-hidden">
<div className="w-full">
@@ -2149,11 +2154,13 @@ export function ChatPage({
${
!untoggled && (showHistorySidebar || toggledSidebar)
? "opacity-100 w-[250px] translate-x-0"
: "opacity-0 w-[200px] pointer-events-none -translate-x-10"
: "opacity-0 w-[250px] pointer-events-none -translate-x-10"
}`}
>
<div className="w-full relative">
<HistorySidebar
setShowAssistantsModal={setShowAssistantsModal}
assistants={assistants}
explicitlyUntoggle={explicitlyUntoggle}
stopGenerating={stopGenerating}
reset={() => setMessage("")}
@@ -2162,62 +2169,69 @@ export function ChatPage({
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
backgroundToggled={toggledSidebar || showHistorySidebar}
currentAssistantId={liveAssistant?.id}
existingChats={chatSessions}
currentChatSession={selectedChatSession}
folders={folders}
openedFolders={openedFolders}
removeToggle={removeToggle}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
showDeleteAllModal={() => setShowDeleteAllModal(true)}
/>
</div>
</div>
</div>
{!settings?.isMobile && retrievalEnabled && (
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
flex-none
<div
className={`
flex-none
fixed
right-0
z-[1000]
bg-background
left-0
z-40
bg-background-100
h-screen
transition-all
bg-opacity-80
duration-300
ease-in-out
${documentSidebarToggled && "opacity-100 w-[350px]"}`}
></div>
</div>
</div>
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
flex-none
fixed
right-0
z-[1000]
h-screen
transition-all
duration-300
ease-in-out
bg-transparent
transition-all
bg-opacity-80
duration-300
ease-in-out
h-full
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
`}
>
<ChatFilters
setPresentingDocument={setPresentingDocument}
modal={false}
filterManager={filterManager}
ccPairs={ccPairs}
tags={tags}
documentSets={documentSets}
ref={innerSidebarElementRef}
showFilters={filtersToggled}
closeSidebar={() => setDocumentSidebarToggled(false)}
selectedMessage={aiMessage}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
clearSelectedDocuments={clearSelectedDocuments}
selectedDocumentTokens={selectedDocumentTokens}
maxTokens={maxTokens}
initialWidth={400}
isOpen={documentSidebarToggled}
/>
</div>
)}
>
<DocumentResults
setPresentingDocument={setPresentingDocument}
modal={false}
ref={innerSidebarElementRef}
closeSidebar={() =>
setTimeout(() => setDocumentSidebarToggled(false), 300)
}
selectedMessage={aiMessage}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
clearSelectedDocuments={clearSelectedDocuments}
selectedDocumentTokens={selectedDocumentTokens}
maxTokens={maxTokens}
initialWidth={400}
isOpen={documentSidebarToggled}
/>
</div>
<BlurBackground
visible={!untoggled && (showHistorySidebar || toggledSidebar)}
@@ -2239,15 +2253,19 @@ export function ChatPage({
? setSharingModalVisible
: undefined
}
documentSidebarToggled={documentSidebarToggled}
toggleSidebar={toggleSidebar}
currentChatSession={selectedChatSession}
documentSidebarToggled={documentSidebarToggled}
hideUserDropdown={user?.is_anonymous_user}
/>
)}
{documentSidebarInitialWidth !== undefined && isReady ? (
<Dropzone onDrop={handleImageUpload} noClick>
<Dropzone
key={currentSessionId()}
onDrop={handleImageUpload}
noClick
>
{({ getRootProps }) => (
<div className="flex h-full w-full">
{!settings?.isMobile && (
@@ -2275,7 +2293,7 @@ export function ChatPage({
className={`w-full h-[calc(100vh-160px)] flex flex-col default-scrollbar overflow-y-auto overflow-x-hidden relative`}
ref={scrollableDivRef}
>
{liveAssistant && onAssistantChange && (
{liveAssistant && (
<div className="z-20 fixed top-0 pointer-events-none left-0 w-full flex justify-center overflow-visible">
{!settings?.isMobile && (
<div
@@ -2292,43 +2310,16 @@ export function ChatPage({
`}
></div>
)}
<AssistantSelector
isMobile={settings?.isMobile!}
liveAssistant={liveAssistant}
onAssistantChange={onAssistantChange}
llmOverrideManager={llmOverrideManager}
/>
{!settings?.isMobile && (
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
flex-none
overflow-y-hidden
transition-all
duration-300
ease-in-out
h-full
pointer-events-none
${
documentSidebarToggled && retrievalEnabled
? "w-[400px]"
: "w-[0px]"
}
`}
></div>
)}
</div>
)}
{/* ChatBanner is a custom banner that displays a admin-specified message at
the top of the chat page. Oly used in the EE version of the app. */}
{messageHistory.length === 0 &&
!isFetchingChatMessages &&
currentSessionChatState == "input" &&
!loadingError && (
<div className="h-full w-[95%] mx-auto mt-12 flex flex-col justify-center items-center">
!loadingError &&
!submittedMessage && (
<div className="h-full w-[95%] mx-auto flex flex-col justify-center items-center">
<ChatIntro selectedPersona={liveAssistant} />
<StarterMessages
@@ -2339,36 +2330,17 @@ export function ChatPage({
})
}
/>
{!isFetchingChatMessages &&
currentSessionChatState == "input" &&
!loadingError &&
allAssistants.length > 1 && (
<div className="mx-auto px-4 w-full max-w-[750px] flex flex-col items-center">
<Separator className="mx-2 w-full my-12" />
<div className="text-sm text-black font-medium mb-4">
Recent Assistants
</div>
<AssistantBanner
mobile={settings?.isMobile}
recentAssistants={recentAssistants}
liveAssistant={liveAssistant}
allAssistants={allAssistants}
onAssistantChange={onAssistantChange}
/>
</div>
)}
</div>
)}
<div
key={currentSessionId()}
className={
"-ml-4 w-full mx-auto " +
"absolute mobile:top-0 desktop:top-12 left-0 " +
"desktop:-ml-4 w-full mx-auto " +
"absolute mobile:top-0 desktop:top-0 left-0 " +
(settings?.enterpriseSettings
?.two_lines_for_chat_header
? "mt-20 "
: "mt-8") +
? "pt-20 "
: "pt-8") +
(hasPerformedInitialScroll ? "" : "invisible")
}
>
@@ -2492,6 +2464,11 @@ export function ChatPage({
}
>
<AIMessage
toggledDocumentSidebar={
documentSidebarToggled &&
selectedMessageForDocDisplay ==
message.messageId
}
setPresentingDocument={
setPresentingDocument
}
@@ -2500,8 +2477,7 @@ export function ChatPage({
selectedMessageForDocDisplay
}
documentSelectionToggled={
documentSidebarToggled &&
!filtersToggled
documentSidebarToggled
}
continueGenerating={
i == messageHistory.length - 1 &&
@@ -2548,6 +2524,7 @@ export function ChatPage({
) {
toggleDocumentSidebar();
}
setSelectedMessageForDocDisplay(
message.messageId
);
@@ -2774,19 +2751,21 @@ export function ChatPage({
</div>
)}
<ChatInputBar
toggleDocumentSidebar={toggleDocumentSidebar}
availableSources={sources}
availableDocumentSets={documentSets}
availableTags={tags}
filterManager={filterManager}
llmOverrideManager={llmOverrideManager}
removeDocs={() => {
clearSelectedDocuments();
}}
showDocs={() => {
setFiltersToggled(false);
setDocumentSidebarToggled(true);
}}
retrievalEnabled={retrievalEnabled}
showConfigureAPIKey={() =>
setShowApiKeyModal(true)
}
chatState={currentSessionChatState}
stopGenerating={stopGenerating}
openModelSettings={() => setSettingsToggled(true)}
selectedDocuments={selectedDocuments}
// assistant stuff
selectedAssistant={liveAssistant}
@@ -2796,12 +2775,8 @@ export function ChatPage({
message={message}
setMessage={setMessage}
onSubmit={onSubmit}
filterManager={filterManager}
files={currentMessageFiles}
setFiles={setCurrentMessageFiles}
toggleFilters={
retrievalEnabled ? toggleFilters : undefined
}
handleFileUpload={handleImageUpload}
textAreaRef={textAreaRef}
/>
@@ -2831,23 +2806,20 @@ export function ChatPage({
</div>
</div>
</div>
{!settings?.isMobile && (
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
flex-none
overflow-y-hidden
transition-all
bg-opacity-80
duration-300
ease-in-out
${
documentSidebarToggled && retrievalEnabled
? "w-[400px]"
: "w-[0px]"
}
h-full
${documentSidebarToggled ? "w-[350px]" : "w-[0px]"}
`}
></div>
)}
></div>
</div>
)}
</Dropzone>

View File

@@ -49,10 +49,10 @@ export function RegenerateDropdown({
border
rounded-lg
flex
flex-col
flex-col
mx-2
bg-background
${maxHeight || "max-h-96"}
${maxHeight || "max-h-72"}
overflow-y-auto
overscroll-contain relative`}
>
@@ -62,8 +62,8 @@ export function RegenerateDropdown({
top-0
flex
bg-background
font-bold
px-3
font-medium
px-2
text-sm
py-1.5
"

View File

@@ -1,17 +1,24 @@
"use client";
import { useChatContext } from "@/components/context/ChatContext";
import { ChatPage } from "./ChatPage";
import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper";
export default function WrappedChat({
initiallyToggled,
firstMessage,
}: {
initiallyToggled: boolean;
firstMessage?: string;
}) {
const { toggledSidebar } = useChatContext();
return (
<FunctionalWrapper
initiallyToggled={initiallyToggled}
initiallyToggled={toggledSidebar}
content={(toggledSidebar, toggle) => (
<ChatPage toggle={toggle} toggledSidebar={toggledSidebar} />
<ChatPage
toggle={toggle}
toggledSidebar={toggledSidebar}
firstMessage={firstMessage}
/>
)}
/>
);

View File

@@ -77,18 +77,21 @@ export function ChatDocumentDisplay({
const hasMetadata =
document.updated_at || Object.keys(document.metadata).length > 0;
return (
<div
className={`max-w-[400px] opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}
className={`desktop:max-w-[400px] opacity-100 ${
modal ? "w-[90vw]" : "w-full"
}`}
>
<div
className={`flex relative flex-col gap-0.5 rounded-xl mx-2 my-1 ${
isSelected ? "bg-gray-200" : "hover:bg-background-125"
className={`flex relative flex-col px-3 py-2.5 gap-0.5 rounded-xl mx-2 my-1 ${
isSelected ? "bg-[#ebe7de]" : "bg- hover:bg-[#ebe7de]/80"
}`}
>
<button
onClick={() => openDocument(document, setPresentingDocument)}
className="cursor-pointer text-left flex flex-col px-2 py-1.5"
className="cursor-pointer text-left flex flex-col"
>
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
{document.is_internet || document.source_type === "web" ? (

View File

@@ -1,200 +0,0 @@
import { OnyxDocument } from "@/lib/search/interfaces";
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
import { usePopup } from "@/components/admin/connectors/Popup";
import { removeDuplicateDocs } from "@/lib/documentUtils";
import { Message } from "../interfaces";
import {
Dispatch,
ForwardedRef,
forwardRef,
SetStateAction,
useEffect,
useState,
} from "react";
import { FilterManager } from "@/lib/hooks";
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
import { SourceSelector } from "../shared_chat_search/SearchFilters";
import { XIcon } from "@/components/icons/icons";
interface ChatFiltersProps {
filterManager?: FilterManager;
closeSidebar: () => void;
selectedMessage: Message | null;
selectedDocuments: OnyxDocument[] | null;
toggleDocumentSelection: (document: OnyxDocument) => void;
clearSelectedDocuments: () => void;
selectedDocumentTokens: number;
maxTokens: number;
initialWidth: number;
isOpen: boolean;
isSharedChat?: boolean;
modal: boolean;
ccPairs: CCPairBasicInfo[];
tags: Tag[];
documentSets: DocumentSet[];
showFilters: boolean;
setPresentingDocument: Dispatch<SetStateAction<OnyxDocument | null>>;
}
export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
(
{
closeSidebar,
modal,
selectedMessage,
selectedDocuments,
filterManager,
toggleDocumentSelection,
clearSelectedDocuments,
selectedDocumentTokens,
maxTokens,
initialWidth,
isSharedChat,
isOpen,
ccPairs,
tags,
setPresentingDocument,
documentSets,
showFilters,
},
ref: ForwardedRef<HTMLDivElement>
) => {
const { popup, setPopup } = usePopup();
const [delayedSelectedDocumentCount, setDelayedSelectedDocumentCount] =
useState(0);
useEffect(() => {
const timer = setTimeout(
() => {
setDelayedSelectedDocumentCount(selectedDocuments?.length || 0);
},
selectedDocuments?.length == 0 ? 1000 : 0
);
return () => clearTimeout(timer);
}, [selectedDocuments]);
const selectedDocumentIds =
selectedDocuments?.map((document) => document.document_id) || [];
const currentDocuments = selectedMessage?.documents || null;
const dedupedDocuments = removeDuplicateDocs(currentDocuments || []);
const tokenLimitReached = selectedDocumentTokens > maxTokens - 75;
const hasSelectedDocuments = selectedDocumentIds.length > 0;
return (
<div
id="onyx-chat-sidebar"
className={`relative bg-background max-w-full ${
!modal ? "border-l h-full border-sidebar-border" : ""
}`}
onClick={(e) => {
if (e.target === e.currentTarget) {
closeSidebar();
}
}}
>
<div
className={`ml-auto h-full relative sidebar transition-all duration-300
${
isOpen
? "opacity-100 translate-x-0"
: "opacity-0 translate-x-[10%]"
}`}
style={{
width: modal ? undefined : initialWidth,
}}
>
<div className="flex flex-col h-full">
{popup}
<div className="p-4 flex justify-between items-center">
<h2 className="text-xl font-bold text-text-900">
{showFilters ? "Filters" : "Sources"}
</h2>
<button
onClick={closeSidebar}
className="text-sm text-primary-600 mr-2 hover:text-primary-800 transition-colors duration-200 ease-in-out"
>
<XIcon className="w-4 h-4" />
</button>
</div>
<div className="border-b border-divider-history-sidebar-bar mx-3" />
<div className="overflow-y-auto -mx-1 sm:mx-0 flex-grow gap-y-0 default-scrollbar dark-scrollbar flex flex-col">
{showFilters ? (
<SourceSelector
{...filterManager!}
modal={modal}
tagsOnLeft={true}
filtersUntoggled={false}
availableDocumentSets={documentSets}
existingSources={ccPairs.map((ccPair) => ccPair.source)}
availableTags={tags}
/>
) : (
<>
{dedupedDocuments.length > 0 ? (
dedupedDocuments.map((document, ind) => (
<div
key={document.document_id}
className={`${
ind === dedupedDocuments.length - 1
? ""
: "border-b border-border-light w-full"
}`}
>
<ChatDocumentDisplay
setPresentingDocument={setPresentingDocument}
closeSidebar={closeSidebar}
modal={modal}
document={document}
isSelected={selectedDocumentIds.includes(
document.document_id
)}
handleSelect={(documentId) => {
toggleDocumentSelection(
dedupedDocuments.find(
(doc) => doc.document_id === documentId
)!
);
}}
hideSelection={isSharedChat}
tokenLimitReached={tokenLimitReached}
/>
</div>
))
) : (
<div className="mx-3" />
)}
</>
)}
</div>
</div>
{!showFilters && (
<div
className={`sticky bottom-4 w-full left-0 flex justify-center transition-opacity duration-300 ${
hasSelectedDocuments
? "opacity-100"
: "opacity-0 pointer-events-none"
}`}
>
<button
className="text-sm font-medium py-2 px-4 rounded-full transition-colors bg-gray-900 text-white"
onClick={clearSelectedDocuments}
>
{`Remove ${
delayedSelectedDocumentCount > 0
? delayedSelectedDocumentCount
: ""
} Source${delayedSelectedDocumentCount > 1 ? "s" : ""}`}
</button>
</div>
)}
</div>
</div>
);
}
);
ChatFilters.displayName = "ChatFilters";

View File

@@ -0,0 +1,166 @@
import { OnyxDocument } from "@/lib/search/interfaces";
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
import { usePopup } from "@/components/admin/connectors/Popup";
import { removeDuplicateDocs } from "@/lib/documentUtils";
import { Message } from "../interfaces";
import {
Dispatch,
ForwardedRef,
forwardRef,
SetStateAction,
useEffect,
useState,
} from "react";
import { SourcesIcon, XIcon } from "@/components/icons/icons";
interface DocumentResultsProps {
closeSidebar: () => void;
selectedMessage: Message | null;
selectedDocuments: OnyxDocument[] | null;
toggleDocumentSelection: (document: OnyxDocument) => void;
clearSelectedDocuments: () => void;
selectedDocumentTokens: number;
maxTokens: number;
initialWidth: number;
isOpen: boolean;
isSharedChat?: boolean;
modal: boolean;
setPresentingDocument: Dispatch<SetStateAction<OnyxDocument | null>>;
}
export const DocumentResults = forwardRef<HTMLDivElement, DocumentResultsProps>(
(
{
closeSidebar,
modal,
selectedMessage,
selectedDocuments,
toggleDocumentSelection,
clearSelectedDocuments,
selectedDocumentTokens,
maxTokens,
initialWidth,
isSharedChat,
isOpen,
setPresentingDocument,
},
ref: ForwardedRef<HTMLDivElement>
) => {
const { popup, setPopup } = usePopup();
const [delayedSelectedDocumentCount, setDelayedSelectedDocumentCount] =
useState(0);
const handleOutsideClick = (event: MouseEvent) => {
const sidebar = document.getElementById("onyx-chat-sidebar");
if (sidebar && !sidebar.contains(event.target as Node)) {
closeSidebar();
}
};
useEffect(() => {
const timer = setTimeout(
() => {
setDelayedSelectedDocumentCount(selectedDocuments?.length || 0);
},
selectedDocuments?.length == 0 ? 1000 : 0
);
return () => clearTimeout(timer);
}, [selectedDocuments]);
const selectedDocumentIds =
selectedDocuments?.map((document) => document.document_id) || [];
const currentDocuments = selectedMessage?.documents || null;
const dedupedDocuments = removeDuplicateDocs(currentDocuments || []);
const tokenLimitReached = selectedDocumentTokens > maxTokens - 75;
const hasSelectedDocuments = selectedDocumentIds.length > 0;
return (
<>
<div
id="onyx-chat-sidebar"
className={`relative -mb-8 bg-background max-w-full ${
!modal ? "border-l border-t h-[105vh] border-sidebar-border" : ""
}`}
onClick={(e) => {
if (e.target === e.currentTarget) {
closeSidebar();
}
}}
>
<div
className={`ml-auto h-full relative sidebar transition-transform ease-in-out duration-300
${isOpen ? " translate-x-0" : " translate-x-[10%]"}`}
style={{
width: modal ? undefined : initialWidth,
}}
>
<div className="flex flex-col h-full">
{popup}
<div className="p-4 flex items-center justify-between gap-x-2">
<div className="flex items-center gap-x-2">
{/* <SourcesIcon size={32} /> */}
<h2 className="text-xl font-bold text-text-900">Sources</h2>
</div>
<button className="my-auto" onClick={closeSidebar}>
<XIcon size={16} />
</button>
</div>
<div className="border-b border-divider-history-sidebar-bar mx-3" />
<div className="overflow-y-auto h-fit mb-8 pb-8 -mx-1 sm:mx-0 flex-grow gap-y-0 default-scrollbar dark-scrollbar flex flex-col">
{dedupedDocuments.length > 0 ? (
dedupedDocuments.map((document, ind) => (
<div key={document.document_id} className="w-full">
<ChatDocumentDisplay
setPresentingDocument={setPresentingDocument}
closeSidebar={closeSidebar}
modal={modal}
document={document}
isSelected={selectedDocumentIds.includes(
document.document_id
)}
handleSelect={(documentId) => {
toggleDocumentSelection(
dedupedDocuments.find(
(doc) => doc.document_id === documentId
)!
);
}}
hideSelection={isSharedChat}
tokenLimitReached={tokenLimitReached}
/>
</div>
))
) : (
<div className="mx-3" />
)}
</div>
</div>
<div
className={`sticky bottom-4 w-full left-0 flex justify-center transition-opacity duration-300 ${
hasSelectedDocuments
? "opacity-100"
: "opacity-0 pointer-events-none"
}`}
>
<button
className="text-sm font-medium py-2 px-4 rounded-full transition-colors bg-neutral-900 text-white"
onClick={clearSelectedDocuments}
>
{`Remove ${
delayedSelectedDocumentCount > 0
? delayedSelectedDocumentCount
: ""
} Source${delayedSelectedDocumentCount > 1 ? "s" : ""}`}
</button>
</div>
</div>
</div>
</>
);
}
);
DocumentResults.displayName = "DocumentResults";

View File

@@ -44,7 +44,7 @@ export function InputBarPreviewImageProvider({
return (
<div
className="h-10 relative"
className="h-6 relative"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
@@ -111,27 +111,28 @@ export function InputBarPreview({
z-0
"
>
<FiLoader className="animate-spin text-white" />
<FiLoader size={12} className="animate-spin text-white" />
</div>
)}
<div
className={`
flex
items-center
p-2
px-2
bg-hover
border
gap-x-1.5
border-border
rounded-md
box-border
h-10
h-8
`}
>
<div className="flex-shrink-0">
<div
className="
w-6
h-6
w-5
h-5
bg-document
flex
items-center
@@ -139,33 +140,31 @@ export function InputBarPreview({
rounded-md
"
>
<FiFileText className="w-4 h-4 text-white" />
<FiFileText size={12} className="text-white" />
</div>
</div>
<div className="ml-2 relative">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
ref={fileNameRef}
className={`font-medium text-sm line-clamp-1 break-all ellipses max-w-48`}
>
{file.name}
</div>
</TooltipTrigger>
<TooltipContent side="top" align="start">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
ref={fileNameRef}
className={`font-medium text-sm line-clamp-1 break-all ellipses max-w-48`}
>
{file.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</TooltipTrigger>
<TooltipContent side="top" align="start">
{file.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<button
onClick={onDelete}
className="
cursor-pointer
border-none
bg-hover
p-1
rounded-full
z-10
"

View File

@@ -24,25 +24,26 @@ export function DocumentPreview({
return (
<div
className={`
${alignBubble && "w-64"}
${alignBubble && "min-w-52 max-w-48"}
flex
items-center
p-3
bg-hover
bg-hover-light/50
border
border-border
rounded-lg
box-border
h-20
py-4
h-12
hover:shadow-sm
transition-all
px-2
`}
>
<div className="flex-shrink-0">
<div
className="
w-14
h-14
w-8
h-8
bg-document
flex
items-center
@@ -53,10 +54,10 @@ export function DocumentPreview({
hover:bg-document-dark
"
>
<FiFileText className="w-7 h-7 text-white" />
<FiFileText className="w-5 h-5 text-white" />
</div>
</div>
<div className="ml-4 flex-grow">
<div className="ml-2 h-8 flex flex-col flex-grow">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -74,7 +75,7 @@ export function DocumentPreview({
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="text-subtle text-xs mt-1">Document</div>
<div className="text-subtle text-xs">Document</div>
</div>
{open && (
<button

View File

@@ -20,19 +20,18 @@ export function InputBarPreviewImage({ fileId }: { fileId: string }) {
border-none
flex
items-center
p-2
bg-hover
border
border-border
rounded-md
box-border
h-10
h-6
`}
>
<img
alt="preview"
onClick={() => setFullImageShowing(true)}
className="h-8 w-8 object-cover rounded-lg bg-background cursor-pointer"
className="h-6 w-6 object-cover rounded-lg bg-background cursor-pointer"
src={buildImgUrl(fileId)}
/>
</div>

View File

@@ -0,0 +1,274 @@
import React, {
useState,
useRef,
useEffect,
ReactNode,
useCallback,
forwardRef,
} from "react";
import { Folder } from "./interfaces";
import { ChatSession } from "../interfaces";
import {
FiChevronDown,
FiChevronRight,
FiEdit,
FiTrash2,
FiCheck,
FiX,
} from "react-icons/fi";
import { Caret } from "@/components/icons/icons";
import { addChatToFolder, deleteFolder } from "./FolderManagement";
import { PencilIcon } from "lucide-react";
import { Popover } from "@/components/popover/Popover";
import { useChatContext } from "@/components/context/ChatContext";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface FolderDropdownProps {
folder: Folder;
currentChatId?: string;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
closeSidebar?: () => void;
onEdit?: (folderId: number, newName: string) => void;
onDelete?: (folderId: number) => void;
onDrop?: (folderId: number, chatSessionId: string) => void;
children?: ReactNode;
}
export const FolderDropdown = forwardRef<HTMLDivElement, FolderDropdownProps>(
(
{
folder,
currentChatId,
showShareModal,
closeSidebar,
onEdit,
onDrop,
children,
},
ref
) => {
const [isOpen, setIsOpen] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [newFolderName, setNewFolderName] = useState(folder.folder_name);
const [isHovered, setIsHovered] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const [isDeletePopoverOpen, setIsDeletePopoverOpen] = useState(false);
const editingRef = useRef<HTMLDivElement>(null);
const { refreshFolders } = useChatContext();
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: folder.folder_id?.toString() ?? "" });
const style: React.CSSProperties = {
transform: transform
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
: undefined,
transition,
zIndex: isDragging ? 9999 : undefined,
position: isDragging ? "absolute" : "relative",
};
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
const handleEdit = useCallback(() => {
if (newFolderName && folder.folder_id !== undefined && onEdit) {
onEdit(folder.folder_id, newFolderName);
setIsEditing(false);
}
}, [newFolderName, folder.folder_id, onEdit]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
editingRef.current &&
!editingRef.current.contains(event.target as Node) &&
isEditing
) {
if (newFolderName !== folder.folder_name) {
handleEdit();
} else {
setIsEditing(false);
}
}
};
if (isEditing) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isEditing, newFolderName, folder.folder_name, handleEdit]);
const handleDeleteClick = useCallback(() => {
setIsDeletePopoverOpen(true);
}, []);
const handleCancelDelete = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDeletePopoverOpen(false);
}, []);
const handleConfirmDelete = useCallback(
async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (folder.folder_id !== undefined) {
await deleteFolder(folder.folder_id);
}
await refreshFolders();
setIsDeletePopoverOpen(false);
},
[folder.folder_id, refreshFolders]
);
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
};
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const chatSessionId = e.dataTransfer.getData("text/plain");
if (folder.folder_id && onDrop) {
onDrop(folder.folder_id, chatSessionId);
}
},
[folder.folder_id, onDrop]
);
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
className="overflow-visible w-full"
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<div
ref={ref}
className="flex overflow-visible items-center w-full text-[#6c6c6c] rounded-md p-1 relative"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<button
className="flex overflow-hidden items-center flex-grow"
onClick={() => !isEditing && setIsOpen(!isOpen)}
{...(isEditing ? {} : listeners)}
>
{isOpen ? (
<Caret size={16} className="mr-1" />
) : (
<Caret size={16} className="-rotate-90 mr-1" />
)}
{isEditing ? (
<div ref={editingRef} className="flex-grow z-[9999] relative">
<input
ref={inputRef}
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
className="text-sm font-medium bg-transparent outline-none w-full pb-1 border-b border-[#6c6c6c] transition-colors duration-200"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleEdit();
}
}}
onClick={(e) => e.stopPropagation()}
/>
</div>
) : (
<div className="flex items-center">
<span className="text-sm font-medium">
{folder.folder_name}
</span>
</div>
)}
</button>
{isHovered && !isEditing && folder.folder_id && (
<button
onClick={(e) => {
e.stopPropagation();
setIsEditing(true);
}}
className="ml-auto px-1"
>
<PencilIcon size={14} />
</button>
)}
{(isHovered || isDeletePopoverOpen) &&
!isEditing &&
folder.folder_id && (
<Popover
open={isDeletePopoverOpen}
onOpenChange={setIsDeletePopoverOpen}
content={
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteClick();
}}
className="px-1"
>
<FiTrash2 size={14} />
</button>
}
popover={
<div className="p-3 w-64 border border-border rounded-lg bg-background z-50">
<p className="text-sm mb-3">
Are you sure you want to delete this folder?
</p>
<div className="flex justify-center gap-2">
<button
className="px-3 py-1 text-sm bg-gray-200 rounded"
onClick={handleCancelDelete}
>
Cancel
</button>
<button
className="px-3 py-1 text-sm bg-red-500 text-white rounded"
onClick={handleConfirmDelete}
>
Delete
</button>
</div>
</div>
}
requiresContentPadding
sideOffset={6}
/>
)}
{isEditing && (
<div className="flex -my-1 z-[9999]">
<button onClick={handleEdit} className="p-1">
<FiCheck size={14} />
</button>
<button onClick={() => setIsEditing(false)} className="p-1">
<FiX size={14} />
</button>
</div>
)}
</div>
{isOpen && (
<div className="overflow-visible mr-3 ml-1 mt-1">{children}</div>
)}
</div>
);
}
);
FolderDropdown.displayName = "FolderDropdown";

View File

@@ -25,6 +25,7 @@ import Cookies from "js-cookie";
import { Popover } from "@/components/popover/Popover";
import { ChatSession } from "../interfaces";
import { useChatContext } from "@/components/context/ChatContext";
const FolderItem = ({
folder,
currentChatId,
@@ -62,11 +63,11 @@ const FolderItem = ({
? JSON.parse(openedFoldersCookieVal)
: {};
if (newIsExpanded) {
openedFolders[folder.folder_id] = true;
openedFolders[folder.folder_id!] = true;
} else {
setShowDeleteConfirm(false);
delete openedFolders[folder.folder_id];
delete openedFolders[folder.folder_id!];
}
Cookies.set("openedFolders", JSON.stringify(openedFolders));
}
@@ -91,7 +92,7 @@ const FolderItem = ({
const saveFolderName = async (continueEditing?: boolean) => {
try {
await updateFolderName(folder.folder_id, editedFolderName);
await updateFolderName(folder.folder_id!, editedFolderName);
if (!continueEditing) {
setIsEditing(false);
}
@@ -112,7 +113,7 @@ const FolderItem = ({
const confirmDelete = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
try {
await deleteFolder(folder.folder_id);
await deleteFolder(folder.folder_id!);
router.refresh();
} catch (error) {
setPopup({ message: "Failed to delete folder", type: "error" });
@@ -155,7 +156,7 @@ const FolderItem = ({
setIsDragOver(false);
const chatSessionId = event.dataTransfer.getData(CHAT_SESSION_ID_KEY);
try {
await addChatToFolder(folder.folder_id, chatSessionId);
await addChatToFolder(folder.folder_id!, chatSessionId);
await refreshChatSessions();
router.refresh();
} catch (error) {
@@ -296,7 +297,7 @@ const FolderItem = ({
{/* Expanded Folder Content */}
{isExpanded && folders && (
<div className={"ml-2 pl-2 border-l border-border"}>
<div className={"mr-4 pl-2 w-full border-l border-border"}>
{folders.map((chatSession) => (
<ChatSessionDisplay
key={chatSession.id}
@@ -341,7 +342,7 @@ export const FolderList = ({
currentChatId={currentChatId}
initiallySelected={newFolderId == folder.folder_id}
isInitiallyExpanded={
openedFolders ? openedFolders[folder.folder_id] || false : false
openedFolders ? openedFolders[folder.folder_id!] || false : false
}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}

View File

@@ -78,3 +78,19 @@ export async function updateFolderName(
throw new Error("Failed to update folder name");
}
}
// Function to update folder display priorities
export async function updateFolderDisplayPriorities(
displayPriorityMap: Record<number, number>
): Promise<void> {
const response = await fetch(`/api/folder/reorder`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ display_priority_map: displayPriorityMap }),
});
if (!response.ok) {
throw new Error("Failed to update folder display priorities");
}
}

View File

@@ -1,7 +1,7 @@
import { ChatSession } from "../interfaces";
export interface Folder {
folder_id: number;
folder_id?: number;
folder_name: string;
display_priority: number;
chat_sessions: ChatSession[];

View File

@@ -0,0 +1,313 @@
import React, { useState, useEffect } from "react";
import { InputPrompt } from "@/app/chat/interfaces";
import { Button } from "@/components/ui/button";
import { TrashIcon, PlusIcon } from "@/components/icons/icons";
import { MoreVertical, CheckIcon, XIcon } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
import { usePopup } from "@/components/admin/connectors/Popup";
import { BackButton } from "@/components/BackButton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { SourceChip } from "../input/ChatInputBar";
export default function InputPrompts() {
const [inputPrompts, setInputPrompts] = useState<InputPrompt[]>([]);
const [editingPromptId, setEditingPromptId] = useState<number | null>(null);
const [newPrompt, setNewPrompt] = useState<Partial<InputPrompt>>({});
const [isCreatingNew, setIsCreatingNew] = useState(false);
const { popup, setPopup } = usePopup();
useEffect(() => {
fetchInputPrompts();
}, []);
const fetchInputPrompts = async () => {
try {
const response = await fetch("/api/input_prompt");
if (response.ok) {
const data = await response.json();
setInputPrompts(data);
} else {
throw new Error("Failed to fetch prompt shortcuts");
}
} catch (error) {
setPopup({ message: "Failed to fetch prompt shortcuts", type: "error" });
}
};
const isPromptPublic = (prompt: InputPrompt): boolean => {
return prompt.is_public;
};
// UPDATED: Remove partial merging to avoid overwriting fresh data
const handleEdit = (promptId: number) => {
setEditingPromptId(promptId);
};
const handleSave = async (
promptId: number,
updatedPrompt: string,
updatedContent: string
) => {
const promptToUpdate = inputPrompts.find((p) => p.id === promptId);
if (!promptToUpdate || isPromptPublic(promptToUpdate)) return;
try {
const response = await fetch(`/api/input_prompt/${promptId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: updatedPrompt,
content: updatedContent,
active: true,
}),
});
if (!response.ok) {
throw new Error("Failed to update prompt");
}
// Update local state with new values
setInputPrompts((prevPrompts) =>
prevPrompts.map((prompt) =>
prompt.id === promptId
? { ...prompt, prompt: updatedPrompt, content: updatedContent }
: prompt
)
);
setEditingPromptId(null);
setPopup({ message: "Prompt updated successfully", type: "success" });
} catch (error) {
setPopup({ message: "Failed to update prompt", type: "error" });
}
};
const handleDelete = async (id: number) => {
const promptToDelete = inputPrompts.find((p) => p.id === id);
if (!promptToDelete) return;
try {
let response;
if (isPromptPublic(promptToDelete)) {
// For public prompts, use the hide endpoint
response = await fetch(`/api/input_prompt/${id}/hide`, {
method: "POST",
});
} else {
// For user-created prompts, use the delete endpoint
response = await fetch(`/api/input_prompt/${id}`, {
method: "DELETE",
});
}
if (!response.ok) {
throw new Error("Failed to delete/hide prompt");
}
setInputPrompts((prevPrompts) =>
prevPrompts.filter((prompt) => prompt.id !== id)
);
setPopup({
message: isPromptPublic(promptToDelete)
? "Prompt hidden successfully"
: "Prompt deleted successfully",
type: "success",
});
} catch (error) {
setPopup({ message: "Failed to delete/hide prompt", type: "error" });
}
};
const handleCreate = async () => {
try {
const response = await fetch("/api/input_prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...newPrompt, is_public: false }),
});
if (!response.ok) {
throw new Error("Failed to create prompt");
}
const createdPrompt = await response.json();
setInputPrompts((prevPrompts) => [...prevPrompts, createdPrompt]);
setNewPrompt({});
setIsCreatingNew(false);
setPopup({ message: "Prompt created successfully", type: "success" });
} catch (error) {
setPopup({ message: "Failed to create prompt", type: "error" });
}
};
const PromptCard = ({ prompt }: { prompt: InputPrompt }) => {
const isEditing = editingPromptId === prompt.id;
const [localPrompt, setLocalPrompt] = useState(prompt.prompt);
const [localContent, setLocalContent] = useState(prompt.content);
// Sync local edits with any prompt changes from outside
useEffect(() => {
setLocalPrompt(prompt.prompt);
setLocalContent(prompt.content);
}, [prompt, isEditing]);
const handleLocalEdit = (field: "prompt" | "content", value: string) => {
if (field === "prompt") {
setLocalPrompt(value);
} else {
setLocalContent(value);
}
};
const handleSaveLocal = () => {
handleSave(prompt.id, localPrompt, localContent);
};
return (
<div className="border rounded-lg p-4 mb-4 relative">
{isEditing ? (
<>
<div className="absolute top-2 right-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingPromptId(null);
fetchInputPrompts(); // Revert changes from server
}}
>
<XIcon size={14} />
</Button>
</div>
<div className="flex">
<div className="flex-grow mr-4">
<Textarea
value={localPrompt}
onChange={(e) => handleLocalEdit("prompt", e.target.value)}
className="mb-2 resize-none"
placeholder="Prompt"
/>
<Textarea
value={localContent}
onChange={(e) => handleLocalEdit("content", e.target.value)}
className="resize-vertical min-h-[100px]"
placeholder="Content"
/>
</div>
<div className="flex items-end">
<Button onClick={handleSaveLocal}>
{prompt.id ? "Save" : "Create"}
</Button>
</div>
</div>
</>
) : (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="mb-2 flex gap-x-2 ">
<p className="font-semibold">{prompt.prompt}</p>
{isPromptPublic(prompt) && <SourceChip title="Built-in" />}
</div>
</TooltipTrigger>
{isPromptPublic(prompt) && (
<TooltipContent>
<p>This is a built-in prompt and cannot be edited</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div className="whitespace-pre-wrap">{prompt.content}</div>
<div className="absolute top-2 right-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical size={14} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!isPromptPublic(prompt) && (
<DropdownMenuItem onClick={() => handleEdit(prompt.id)}>
Edit
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => handleDelete(prompt.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
)}
</div>
);
};
return (
<div className="mx-auto max-w-4xl">
<div className="absolute top-4 left-4">
<BackButton />
</div>
{popup}
<div className="flex justify-between items-start mb-6">
<div className="flex flex-col gap-2">
<Title>Prompt Shortcuts</Title>
<Text>
Manage and customize prompt shortcuts for your assistants. Use your
prompt shortcuts by starting a new message / in chat
</Text>
</div>
</div>
{inputPrompts.map((prompt) => (
<PromptCard key={prompt.id} prompt={prompt} />
))}
{isCreatingNew ? (
<div className="space-y-2 border p-4 rounded-md mt-4">
<Textarea
placeholder="Prompt Shortcut (e.g. Summarize)"
value={newPrompt.prompt || ""}
onChange={(e) =>
setNewPrompt({ ...newPrompt, prompt: e.target.value })
}
className="resize-none"
/>
<Textarea
placeholder="Actual Prompt (e.g. Summarize the uploaded document and highlight key points.)"
value={newPrompt.content || ""}
onChange={(e) =>
setNewPrompt({ ...newPrompt, content: e.target.value })
}
className="resize-none"
/>
<div className="flex space-x-2">
<Button onClick={handleCreate}>Create</Button>
<Button variant="ghost" onClick={() => setIsCreatingNew(false)}>
Cancel
</Button>
</div>
</div>
) : (
<Button onClick={() => setIsCreatingNew(true)} className="w-full mt-4">
<PlusIcon size={14} className="mr-2" />
Create New Prompt
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,15 @@
"use client";
import InputPrompts from "./InputPrompts";
export default function InputPromptsPage() {
return (
<div className="w-full py-16">
<div className="px-32">
<div className="mx-auto container">
<InputPrompts />
</div>
</div>
</div>
);
}

View File

@@ -1,18 +1,15 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi";
import React, { useEffect, useRef, useState } from "react";
import { FiPlusCircle, FiPlus, FiInfo, FiX, FiFilter } from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption";
import { Persona } from "@/app/admin/assistants/interfaces";
import LLMPopover from "./LLMPopover";
import { InputPrompt } from "@/app/chat/interfaces";
import { FilterManager } from "@/lib/hooks";
import { FilterManager, LlmOverrideManager } from "@/lib/hooks";
import { useChatContext } from "@/components/context/ChatContext";
import { getFinalLLM } from "@/lib/llm/utils";
import { ChatFileType, FileDescriptor } from "../interfaces";
import {
InputBarPreview,
InputBarPreviewImageProvider,
} from "../files/InputBarPreview";
import {
AssistantsIconSkeleton,
DocumentIcon2,
FileIcon,
SendIcon,
StopGeneratingIcon,
@@ -26,50 +23,105 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Hoverable } from "@/components/Hoverable";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { ChatState } from "../types";
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
import { useAssistants } from "@/components/context/AssistantsContext";
import { XIcon } from "lucide-react";
import FiltersDisplay from "./FilterDisplay";
import { CalendarIcon, XIcon } from "lucide-react";
import { FilterPopup } from "@/components/search/filtering/FilterPopup";
import { DocumentSet, Tag } from "@/lib/types";
import { SourceIcon } from "@/components/SourceIcon";
import { getFormattedDateRangeString } from "@/lib/dateUtils";
import { truncateString } from "@/lib/utils";
import { buildImgUrl } from "../files/images/utils";
import { useUser } from "@/components/user/UserProvider";
const MAX_INPUT_HEIGHT = 200;
export const SourceChip = ({
icon,
title,
onRemove,
onClick,
truncateTitle = true,
}: {
icon?: React.ReactNode;
title: string;
onRemove?: () => void;
onClick?: () => void;
truncateTitle?: boolean;
}) => (
<div
onClick={onClick ? onClick : undefined}
className={`
flex-none
flex
items-center
px-1
bg-gray-background
text-xs
text-text-darker
border
gap-x-1.5
border-border
rounded-md
box-border
gap-x-1
h-6
${onClick ? "cursor-pointer" : ""}
`}
>
{icon}
{truncateTitle ? truncateString(title, 20) : title}
{onRemove && (
<XIcon
size={12}
className="text-text-900 ml-auto cursor-pointer"
onClick={(e: React.MouseEvent<SVGSVGElement>) => {
e.stopPropagation();
onRemove();
}}
/>
)}
</div>
);
interface ChatInputBarProps {
removeDocs: () => void;
openModelSettings: () => void;
showDocs: () => void;
showConfigureAPIKey: () => void;
selectedDocuments: OnyxDocument[];
message: string;
setMessage: (message: string) => void;
stopGenerating: () => void;
onSubmit: () => void;
filterManager: FilterManager;
llmOverrideManager: LlmOverrideManager;
chatState: ChatState;
alternativeAssistant: Persona | null;
// assistants
selectedAssistant: Persona;
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
toggleDocumentSidebar: () => void;
files: FileDescriptor[];
setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[]) => void;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
toggleFilters?: () => void;
filterManager: FilterManager;
availableSources: SourceMetadata[];
availableDocumentSets: DocumentSet[];
availableTags: Tag[];
retrievalEnabled: boolean;
}
export function ChatInputBar({
retrievalEnabled,
removeDocs,
openModelSettings,
showDocs,
toggleDocumentSidebar,
filterManager,
showConfigureAPIKey,
selectedDocuments,
message,
setMessage,
stopGenerating,
onSubmit,
filterManager,
chatState,
// assistants
@@ -81,8 +133,12 @@ export function ChatInputBar({
handleFileUpload,
textAreaRef,
alternativeAssistant,
toggleFilters,
availableSources,
availableDocumentSets,
availableTags,
llmOverrideManager,
}: ChatInputBarProps) {
const { user } = useUser();
useEffect(() => {
const textarea = textAreaRef.current;
if (textarea) {
@@ -111,11 +167,9 @@ export function ChatInputBar({
}
};
const settings = useContext(SettingsContext);
const { finalAssistants: assistantOptions } = useAssistants();
const { llmProviders } = useChatContext();
const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null);
const { llmProviders, inputPrompts } = useChatContext();
const suggestionsRef = useRef<HTMLDivElement | null>(null);
const [showSuggestions, setShowSuggestions] = useState(false);
@@ -165,10 +219,38 @@ export function ChatInputBar({
}
};
const [showPrompts, setShowPrompts] = useState(false);
const hidePrompts = () => {
setTimeout(() => {
setShowPrompts(false);
}, 50);
setTabbingIconIndex(0);
};
const updateInputPrompt = (prompt: InputPrompt) => {
hidePrompts();
setMessage(`${prompt.content}`);
};
const handlePromptInput = (text: string) => {
if (!text.startsWith("/")) {
hidePrompts();
} else {
const promptMatch = text.match(/(?:\s|^)\/(\w*)$/);
if (promptMatch) {
setShowPrompts(true);
} else {
hidePrompts();
}
}
};
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = event.target.value;
setMessage(text);
handleAssistantInput(text);
handlePromptInput(text);
};
const assistantTagOptions = assistantOptions.filter((assistant) =>
@@ -182,32 +264,56 @@ export function ChatInputBar({
const [tabbingIconIndex, setTabbingIconIndex] = useState(0);
const filteredPrompts = inputPrompts.filter(
(prompt) =>
prompt.active &&
prompt.prompt.toLowerCase().startsWith(
message
.slice(message.lastIndexOf("/") + 1)
.split(/\s/)[0]
.toLowerCase()
)
);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
showSuggestions &&
assistantTagOptions.length > 0 &&
((showSuggestions && assistantTagOptions.length > 0) || showPrompts) &&
(e.key === "Tab" || e.key == "Enter")
) {
e.preventDefault();
if (tabbingIconIndex == assistantTagOptions.length && showSuggestions) {
window.open("/assistants/new", "_self");
if (
(tabbingIconIndex == assistantTagOptions.length && showSuggestions) ||
(tabbingIconIndex == filteredPrompts.length && showPrompts)
) {
if (showPrompts) {
window.open("/chat/input-prompts", "_self");
} else {
window.open("/assistants/new", "_self");
}
} else {
const option =
assistantTagOptions[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
updatedTaggedAssistant(option);
if (showPrompts) {
const selectedPrompt =
filteredPrompts[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
updateInputPrompt(selectedPrompt);
} else {
const option =
assistantTagOptions[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
updatedTaggedAssistant(option);
}
}
}
if (!showSuggestions) {
if (!showPrompts && !showSuggestions) {
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setTabbingIconIndex((tabbingIconIndex) =>
Math.min(tabbingIconIndex + 1, assistantTagOptions.length)
Math.min(
tabbingIconIndex + 1,
showPrompts ? filteredPrompts.length : assistantTagOptions.length
)
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
@@ -219,7 +325,7 @@ export function ChatInputBar({
return (
<div id="onyx-chat-input">
<div className="flex justify-center mx-auto">
<div className="flex justify-center mx-auto">
<div
className="
w-[800px]
@@ -231,21 +337,24 @@ export function ChatInputBar({
{showSuggestions && assistantTagOptions.length > 0 && (
<div
ref={suggestionsRef}
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
className="text-sm absolute w-[calc(100%-2rem)] top-0 transform -translate-y-full"
>
<div className="rounded-lg py-1.5 bg-background border border-border-medium shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
<div className="rounded-lg py-1 sm-1.5 bg-background border border-border shadow-lg px-1.5 mt-2 z-10">
{assistantTagOptions.map((currentAssistant, index) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-hover-lightish"
} rounded rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-hover-lightish cursor-pointer`}
tabbingIconIndex == index && "bg-background-dark/75"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-background-dark/90 cursor-pointer`}
onClick={() => {
updatedTaggedAssistant(currentAssistant);
}}
>
<p className="font-bold">{currentAssistant.name}</p>
<p className="line-clamp-1">
<AssistantIcon size={16} assistant={currentAssistant} />
<p className="text-text-darker font-semibold">
{currentAssistant.name}
</p>
<p className="text-text-dark font-light line-clamp-1">
{currentAssistant.id == selectedAssistant.id &&
"(default) "}
{currentAssistant.description}
@@ -257,8 +366,9 @@ export function ChatInputBar({
key={assistantTagOptions.length}
target="_self"
className={`${
tabbingIconIndex == assistantTagOptions.length && "bg-hover"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-hover-lightish cursor-pointer"`}
tabbingIconIndex == assistantTagOptions.length &&
"bg-background-dark/75"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-background-dark/90 cursor-pointer`}
href="/assistants/new"
>
<FiPlus size={17} />
@@ -268,30 +378,70 @@ export function ChatInputBar({
</div>
)}
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
{showPrompts && user?.preferences?.shortcut_enabled && (
<div
ref={suggestionsRef}
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
>
<div className="rounded-lg py-1.5 bg-background border border-border shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPrompts.map(
(currentPrompt: InputPrompt, index: number) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-background-dark/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-dark/90 cursor-pointer`}
onClick={() => {
updateInputPrompt(currentPrompt);
}}
>
<p className="font-bold">{currentPrompt.prompt}:</p>
<p className="text-left flex-grow mr-auto line-clamp-1">
{currentPrompt.content?.trim()}
</p>
</button>
)
)}
<a
key={filteredPrompts.length}
target="_self"
className={`${
tabbingIconIndex == filteredPrompts.length &&
"bg-background-dark/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-dark/90 cursor-pointer`}
href="/chat/input-prompts"
>
<FiPlus size={17} />
<p>Create a new prompt</p>
</a>
</div>
</div>
)}
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
<div className="w-full h-[10px]"></div>
<div
className="
opacity-100
w-full
h-fit
bg-bl
flex
flex-col
border
border-[#E5E7EB]
shadow
border-[#DCDAD4]/60
rounded-lg
text-text-chatbar
bg-background-chatbar
[&:has(textarea:focus)]::ring-1
[&:has(textarea:focus)]::ring-black
"
>
{alternativeAssistant && (
<div className="flex flex-wrap gap-y-1 gap-x-2 px-2 pt-1.5 w-full">
<div className="flex bg-background flex-wrap gap-x-2 px-2 pt-1.5 w-full">
<div
ref={interactionsRef}
className="bg-background-200 p-2 rounded-t-lg items-center flex w-full"
className="p-2 rounded-t-lg items-center flex w-full"
>
<AssistantIcon assistant={alternativeAssistant} />
<p className="ml-3 text-strong my-auto">
@@ -322,58 +472,6 @@ export function ChatInputBar({
</div>
)}
{(selectedDocuments.length > 0 || files.length > 0) && (
<div className="flex gap-x-2 px-2 pt-2">
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
{selectedDocuments.length > 0 && (
<button
onClick={showDocs}
className="flex-none relative overflow-visible flex items-center gap-x-2 h-10 px-3 rounded-lg bg-background-150 hover:bg-background-200 transition-colors duration-300 cursor-pointer max-w-[150px]"
>
<FileIcon size={20} />
<span className="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
{selectedDocuments.length} selected
</span>
<XIcon
onClick={removeDocs}
size={16}
className="text-text-400 hover:text-text-600 ml-auto"
/>
</button>
)}
{files.map((file) => (
<div className="flex-none" key={file.id}>
{file.type === ChatFileType.IMAGE ? (
<InputBarPreviewImageProvider
file={file}
onDelete={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
isUploading={file.isUploading || false}
/>
) : (
<InputBarPreview
file={file}
onDelete={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
isUploading={file.isUploading || false}
/>
)}
</div>
))}
</div>
</div>
)}
<textarea
onPaste={handlePaste}
onKeyDownCapture={handleKeyDown}
@@ -386,8 +484,8 @@ export function ChatInputBar({
resize-none
rounded-lg
border-0
bg-background-chatbar
placeholder:text-text-chatbar-subtle
bg-background
placeholder:text-text-muted
${
textAreaRef.current &&
textAreaRef.current.scrollHeight > MAX_INPUT_HEIGHT
@@ -402,17 +500,17 @@ export function ChatInputBar({
resize-none
px-5
py-4
h-14
`}
autoFocus
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder="Ask me anything.."
placeholder={`Message ${selectedAssistant.name} assistant...`}
value={message}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!showPrompts &&
!showSuggestions &&
!event.shiftKey &&
!(event.nativeEvent as any).isComposing
@@ -425,7 +523,130 @@ export function ChatInputBar({
}}
suppressContentEditableWarning={true}
/>
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
{(selectedDocuments.length > 0 ||
files.length > 0 ||
filterManager.timeRange ||
filterManager.selectedDocumentSets.length > 0 ||
filterManager.selectedTags.length > 0 ||
filterManager.selectedSources.length > 0) && (
<div className="flex gap-x-.5 px-2">
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
{filterManager.timeRange && (
<SourceChip
truncateTitle={false}
key="time-range"
icon={<CalendarIcon size={12} />}
title={`${getFormattedDateRangeString(
filterManager.timeRange.from,
filterManager.timeRange.to
)}`}
onRemove={() => {
filterManager.setTimeRange(null);
}}
/>
)}
{filterManager.selectedDocumentSets.length > 0 &&
filterManager.selectedDocumentSets.map((docSet, index) => (
<SourceChip
key={`doc-set-${index}`}
icon={<DocumentIcon2 size={16} />}
title={docSet}
onRemove={() => {
filterManager.setSelectedDocumentSets(
filterManager.selectedDocumentSets.filter(
(ds) => ds !== docSet
)
);
}}
/>
))}
{filterManager.selectedSources.length > 0 &&
filterManager.selectedSources.map((source, index) => (
<SourceChip
key={`source-${index}`}
icon={
<SourceIcon
sourceType={source.internalName}
iconSize={16}
/>
}
title={source.displayName}
onRemove={() => {
filterManager.setSelectedSources(
filterManager.selectedSources.filter(
(s) => s.internalName !== source.internalName
)
);
}}
/>
))}
{selectedDocuments.length > 0 && (
<SourceChip
key="selected-documents"
onClick={() => {
toggleDocumentSidebar();
}}
icon={<FileIcon size={16} />}
title={`${selectedDocuments.length} selected`}
onRemove={removeDocs}
/>
)}
{files.map((file, index) =>
file.type === ChatFileType.IMAGE ? (
<SourceChip
key={`file-${index}`}
icon={
<img
className="h-full py-.5 object-cover rounded-lg bg-background cursor-pointer"
src={buildImgUrl(file.id)}
/>
}
title={file.name || "File"}
onRemove={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
/>
) : (
// <InputBarPreviewImageProvider
// <InputBarPreviewImageProvider
// key={`file-${index}`}
// file={file}
// onDelete={() => {
// setFiles(
// files.filter(
// (fileInFilter) => fileInFilter.id !== file.id
// )
// );
// }}
// isUploading={file.isUploading || false}
// />
<SourceChip
key={`file-${index}`}
icon={<FileIcon className="text-red-500" size={16} />}
title={file.name || "File"}
onRemove={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
/>
)
)}
</div>
</div>
)}
<div className="flex items-center space-x-1 mr-12 px-4 pb-2">
<ChatInputOption
flexPriority="stiff"
name="File"
@@ -433,7 +654,7 @@ export function ChatInputBar({
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true; // Allow multiple files
input.multiple = true;
input.onchange = (event: any) => {
const files = Array.from(
event?.target?.files || []
@@ -444,25 +665,32 @@ export function ChatInputBar({
};
input.click();
}}
tooltipContent={"Upload files"}
/>
{toggleFilters && (
<ChatInputOption
flexPriority="stiff"
name="Filters"
Icon={FiSearch}
onClick={toggleFilters}
<LLMPopover
llmProviders={llmProviders}
llmOverrideManager={llmOverrideManager}
requiresImageGeneration={false}
currentAssistant={selectedAssistant}
/>
{retrievalEnabled && (
<FilterPopup
availableSources={availableSources}
availableDocumentSets={availableDocumentSets}
availableTags={availableTags}
filterManager={filterManager}
trigger={
<ChatInputOption
flexPriority="stiff"
name="Filters"
Icon={FiFilter}
tooltipContent="Filter your search"
/>
}
/>
)}
{(filterManager.selectedSources.length > 0 ||
filterManager.selectedDocumentSets.length > 0 ||
filterManager.selectedTags.length > 0 ||
filterManager.timeRange) &&
toggleFilters && (
<FiltersDisplay
filterManager={filterManager}
toggleFilters={toggleFilters}
/>
)}
</div>
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
@@ -495,7 +723,7 @@ export function ChatInputBar({
disabled={chatState != "input"}
>
<SendIcon
size={28}
size={26}
className={`text-emphasis text-white p-1 rounded-full ${
chatState == "input" && message
? "bg-submit-background"

View File

@@ -1,5 +1,11 @@
import React, { useState, useRef, useEffect } from "react";
import { ChevronDownIcon, IconProps } from "@/components/icons/icons";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface ChatInputOptionProps {
name?: string;
@@ -23,7 +29,7 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
}) => {
const [isDropupVisible, setDropupVisible] = useState(false);
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
const componentRef = useRef<HTMLDivElement>(null);
const componentRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -43,57 +49,57 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
}, []);
return (
<div
ref={componentRef}
className={`
relative
cursor-pointer
flex
items-center
space-x-2
text-text-700
hover:bg-hover
hover:text-emphasis
p-1.5
rounded-md
${
flexPriority === "shrink" &&
"flex-shrink-100 flex-grow-0 flex-basis-auto min-w-[30px] whitespace-nowrap overflow-hidden"
}
${
flexPriority === "second" &&
"flex-shrink flex-basis-0 min-w-[30px] whitespace-nowrap overflow-hidden"
}
${
flexPriority === "stiff" &&
"flex-none whitespace-nowrap overflow-hidden"
}
`}
title={name}
onClick={onClick}
>
<Icon size={size} className="flex-none" />
<div className="flex items-center gap-x-.5">
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
{toggle && (
<ChevronDownIcon className="flex-none ml-1" size={size - 4} />
)}
</div>
{isTooltipVisible && tooltipContent && (
<div
className="absolute z-10 p-2 text-sm text-white bg-black rounded shadow-lg"
style={{
top: "100%",
left: "50%",
transform: "translateX(-50%)",
marginTop: "0.5rem",
whiteSpace: "nowrap",
}}
>
{tooltipContent}
</div>
)}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
ref={componentRef}
className={`
relative
cursor-pointer
flex
items-center
space-x-1
group
text-text-700
!rounded-lg
hover:bg-background-chat-hover
hover:text-emphasis
py-1.5
px-2
${
flexPriority === "shrink" &&
"flex-shrink-100 flex-grow-0 flex-basis-auto min-w-[30px] whitespace-nowrap overflow-hidden"
}
${
flexPriority === "second" &&
"flex-shrink flex-basis-0 min-w-[30px] whitespace-nowrap overflow-hidden"
}
${
flexPriority === "stiff" &&
"flex-none whitespace-nowrap overflow-hidden"
}
`}
onClick={onClick}
>
<Icon
size={size}
className="h-4 w-4 my-auto text-[#4a4a4a] group-hover:text-text flex-none"
/>
<div className="flex items-center">
{name && (
<span className="text-sm text-[#4a4a4a] group-hover:text-text break-all line-clamp-1">
{name}
</span>
)}
{toggle && (
<ChevronDownIcon className="flex-none ml-1" size={size - 4} />
)}
</div>
</button>
</TooltipTrigger>
<TooltipContent>{tooltipContent}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

View File

@@ -0,0 +1,153 @@
import React from "react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ChatInputOption } from "./ChatInputOption";
import { AnthropicSVG } from "@/components/icons/icons";
import { getDisplayNameForModel } from "@/lib/hooks";
import {
checkLLMSupportsImageInput,
destructureValue,
structureValue,
} from "@/lib/llm/utils";
import {
getProviderIcon,
LLMProviderDescriptor,
} from "@/app/admin/configuration/llm/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { LlmOverrideManager } from "@/lib/hooks";
interface LLMPopoverProps {
llmProviders: LLMProviderDescriptor[];
llmOverrideManager: LlmOverrideManager;
requiresImageGeneration?: boolean;
currentAssistant?: Persona;
}
export default function LLMPopover({
llmProviders,
llmOverrideManager,
requiresImageGeneration,
currentAssistant,
}: LLMPopoverProps) {
const { llmOverride, updateLLMOverride, globalDefault } = llmOverrideManager;
const currentLlm = llmOverride.modelName || globalDefault.modelName;
const llmOptionsByProvider: {
[provider: string]: {
name: string;
value: string;
icon: React.FC<{ size?: number; className?: string }>;
}[];
} = {};
const uniqueModelNames = new Set<string>();
llmProviders.forEach((llmProvider) => {
if (!llmOptionsByProvider[llmProvider.provider]) {
llmOptionsByProvider[llmProvider.provider] = [];
}
(llmProvider.display_model_names || llmProvider.model_names).forEach(
(modelName) => {
if (!uniqueModelNames.has(modelName)) {
uniqueModelNames.add(modelName);
llmOptionsByProvider[llmProvider.provider].push({
name: modelName,
value: structureValue(
llmProvider.name,
llmProvider.provider,
modelName
),
icon: getProviderIcon(llmProvider.provider, modelName),
});
}
}
);
});
const llmOptions = Object.entries(llmOptionsByProvider).flatMap(
([provider, options]) => [...options]
);
const defaultProvider = llmProviders.find(
(llmProvider) => llmProvider.is_default_provider
);
const defaultModelName = defaultProvider?.default_model_name;
const defaultModelDisplayName = defaultModelName
? getDisplayNameForModel(defaultModelName)
: null;
return (
<Popover>
<PopoverTrigger asChild>
<button className="focus:outline-none">
<ChatInputOption
toggle
flexPriority="stiff"
name={getDisplayNameForModel(
llmOverrideManager?.llmOverride.modelName ||
defaultModelDisplayName ||
"Models"
)}
Icon={getProviderIcon(
llmOverrideManager?.llmOverride.provider ||
defaultProvider?.provider ||
"anthropic",
llmOverrideManager?.llmOverride.modelName ||
defaultProvider?.default_model_name ||
"claude-3-5-sonnet-20240620"
)}
tooltipContent="Switch models"
/>
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-64 p-1 bg-background border border-gray-200 rounded-md shadow-lg"
>
<div className="max-h-[300px] overflow-y-auto">
{llmOptions.map(({ name, icon, value }, index) => {
if (!requiresImageGeneration || checkLLMSupportsImageInput(name)) {
return (
<button
key={index}
className={`w-full flex items-center gap-x-2 px-3 py-2 text-sm text-left hover:bg-gray-100 transition-colors duration-150 ${
currentLlm === name
? "bg-gray-100 text-text"
: "text-text-darker"
}`}
onClick={() => updateLLMOverride(destructureValue(value))}
>
{icon({ size: 16, className: "flex-none my-auto " })}
<span className="line-clamp-1 ">
{getDisplayNameForModel(name)}
</span>
{(() => {
if (currentAssistant?.llm_model_version_override === name) {
return (
<span className="flex-none ml-auto text-xs">
(assistant)
</span>
);
} else if (globalDefault.modelName === name) {
return (
<span className="flex-none ml-auto text-xs">
(user default)
</span>
);
}
return null;
})()}
</button>
);
}
return null;
})}
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,244 @@
import React, { useEffect } from "react";
import { FiPlusCircle } from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption";
import { FilterManager } from "@/lib/hooks";
import { ChatFileType, FileDescriptor } from "../interfaces";
import {
InputBarPreview,
InputBarPreviewImageProvider,
} from "../files/InputBarPreview";
import { OpenAIIcon, SendIcon } from "@/components/icons/icons";
import { HorizontalSourceSelector } from "@/components/search/filtering/HorizontalSourceSelector";
import { Tag } from "@/lib/types";
const MAX_INPUT_HEIGHT = 200;
interface ChatInputBarProps {
message: string;
setMessage: (message: string) => void;
onSubmit: () => void;
files: FileDescriptor[];
setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[]) => void;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
filterManager?: FilterManager;
existingSources: string[];
availableDocumentSets: { name: string }[];
availableTags: Tag[];
}
export function SimplifiedChatInputBar({
message,
setMessage,
onSubmit,
files,
setFiles,
handleFileUpload,
textAreaRef,
filterManager,
existingSources,
availableDocumentSets,
availableTags,
}: ChatInputBarProps) {
useEffect(() => {
const textarea = textAreaRef.current;
if (textarea) {
textarea.style.height = "0px";
textarea.style.height = `${Math.min(
textarea.scrollHeight,
MAX_INPUT_HEIGHT
)}px`;
}
}, [message, textAreaRef]);
const handlePaste = (event: React.ClipboardEvent) => {
const items = event.clipboardData?.items;
if (items) {
const pastedFiles = [];
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
if (file) pastedFiles.push(file);
}
}
if (pastedFiles.length > 0) {
event.preventDefault();
handleFileUpload(pastedFiles);
}
}
};
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = event.target.value;
setMessage(text);
};
return (
<div
id="onyx-chat-input"
className="
w-full
relative
mx-auto
"
>
<div
className="
opacity-100
w-full
h-fit
flex
flex-col
border
border-[#E5E7EB]
rounded-lg
relative
text-text-chatbar
bg-background-chatbar
[&:has(textarea:focus)]::ring-1
[&:has(textarea:focus)]::ring-black
"
>
{files.length > 0 && (
<div className="flex gap-x-2 px-2 pt-2">
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
{files.map((file) => (
<div className="flex-none" key={file.id}>
{file.type === ChatFileType.IMAGE ? (
<InputBarPreviewImageProvider
file={file}
onDelete={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
isUploading={file.isUploading || false}
/>
) : (
<InputBarPreview
file={file}
onDelete={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
isUploading={file.isUploading || false}
/>
)}
</div>
))}
</div>
</div>
)}
<textarea
onPaste={handlePaste}
onChange={handleInputChange}
ref={textAreaRef}
className={`
m-0
w-full
shrink
resize-none
rounded-lg
border-0
bg-background-chatbar
placeholder:text-text-chatbar-subtle
${
textAreaRef.current &&
textAreaRef.current.scrollHeight > MAX_INPUT_HEIGHT
? "overflow-y-auto mt-2"
: ""
}
whitespace-normal
break-word
overscroll-contain
outline-none
placeholder-subtle
resize-none
px-5
py-4
h-14
`}
autoFocus
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder="Ask me anything..."
value={message}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!(event.nativeEvent as any).isComposing
) {
event.preventDefault();
if (message) {
onSubmit();
}
}
}}
suppressContentEditableWarning={true}
/>
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
<ChatInputOption
flexPriority="stiff"
name="File"
Icon={FiPlusCircle}
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true; // Allow multiple files
input.onchange = (event: any) => {
const selectedFiles = Array.from(
event?.target?.files || []
) as File[];
if (selectedFiles.length > 0) {
handleFileUpload(selectedFiles);
}
};
input.click();
}}
/>
{filterManager && (
<HorizontalSourceSelector
timeRange={filterManager.timeRange}
setTimeRange={filterManager.setTimeRange}
selectedSources={filterManager.selectedSources}
setSelectedSources={filterManager.setSelectedSources}
selectedDocumentSets={filterManager.selectedDocumentSets}
setSelectedDocumentSets={filterManager.setSelectedDocumentSets}
selectedTags={filterManager.selectedTags}
setSelectedTags={filterManager.setSelectedTags}
existingSources={existingSources}
availableDocumentSets={availableDocumentSets}
availableTags={availableTags}
/>
)}
</div>
</div>
<div className="absolute bottom-2 mobile:right-4 desktop:right-4">
<button
className="cursor-pointer"
onClick={() => {
if (message) {
onSubmit();
}
}}
>
<SendIcon
size={28}
className={`text-emphasis text-white p-1 rounded-full ${
message ? "bg-submit-background" : "bg-disabled-submit-background"
} `}
/>
</button>
</div>
</div>
);
}

View File

@@ -146,3 +146,35 @@ export interface StreamingError {
error: string;
stack_trace: string;
}
export interface InputPrompt {
id: number;
prompt: string;
content: string;
active: boolean;
is_public: boolean;
}
export interface EditPromptModalProps {
onClose: () => void;
promptId: number;
editInputPrompt: (
promptId: number,
values: CreateInputPromptRequest
) => Promise<void>;
}
export interface CreateInputPromptRequest {
prompt: string;
content: string;
}
export interface AddPromptModalProps {
onClose: () => void;
onSubmit: (promptData: CreateInputPromptRequest) => void;
}
export interface PromptData {
id: number;
prompt: string;
content: string;
}

View File

@@ -0,0 +1,66 @@
import { redirect } from "next/navigation";
import { unstable_noStore as noStore } from "next/cache";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { ChatProvider } from "@/components/context/ChatContext";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
noStore();
// Ensure searchParams is an object, even if it's empty
const safeSearchParams = {};
const data = await fetchChatData(
safeSearchParams as { [key: string]: string }
);
if ("redirect" in data) {
redirect(data.redirect);
}
const {
chatSessions,
availableSources,
user,
documentSets,
tags,
llmProviders,
folders,
openedFolders,
toggleSidebar,
defaultAssistantId,
shouldShowWelcomeModal,
ccPairs,
inputPrompts,
} = data;
return (
<>
<InstantSSRAutoRefresh />
<ChatProvider
value={{
inputPrompts,
chatSessions,
toggledSidebar: toggleSidebar,
availableSources,
ccPairs,
documentSets,
tags,
availableDocumentSets: documentSets,
availableTags: tags,
llmProviders,
folders,
openedFolders,
shouldShowWelcomeModal,
defaultAssistantId,
}}
>
{children}
</ChatProvider>
</>
);
}

View File

@@ -1,12 +1,9 @@
import { Citation } from "@/components/search/results/Citation";
import { WebResultIcon } from "@/components/WebResultIcon";
import { LoadedOnyxDocument, OnyxDocument } from "@/lib/search/interfaces";
import { getSourceMetadata, SOURCE_METADATA_MAP } from "@/lib/sources";
import { ValidSources } from "@/lib/types";
import React, { memo } from "react";
import isEqual from "lodash/isEqual";
import { SlackIcon } from "@/components/icons/icons";
import { SourceIcon } from "@/components/SourceIcon";
import { WebResultIcon } from "@/components/WebResultIcon";
export const MemoizedAnchor = memo(
({
@@ -23,22 +20,28 @@ export const MemoizedAnchor = memo(
const match = value.match(/\[(\d+)\]/);
if (match) {
const index = parseInt(match[1], 10) - 1;
const associatedDoc = docs && docs[index];
const associatedDoc = docs?.[index];
if (!associatedDoc) {
return <>{children}</>;
}
const url = associatedDoc?.link
? new URL(associatedDoc.link).origin + "/favicon.ico"
: "";
const icon =
(associatedDoc && (
<SourceIcon sourceType={associatedDoc?.source_type} iconSize={18} />
)) ||
null;
let icon: React.ReactNode = null;
if (associatedDoc.source_type === "web") {
icon = <WebResultIcon url={associatedDoc.link} />;
} else {
icon = (
<SourceIcon sourceType={associatedDoc.source_type} iconSize={18} />
);
}
return (
<MemoizedLink
updatePresentingDocument={updatePresentingDocument}
document={{ ...associatedDoc, icon, url }}
document={{
...associatedDoc,
icon,
url: associatedDoc.link,
}}
>
{children}
</MemoizedLink>
@@ -66,7 +69,6 @@ export const MemoizedLink = memo((props: any) => {
<Citation
url={document?.url}
icon={document?.icon as React.ReactNode}
link={rest?.href}
document={document as LoadedOnyxDocument}
updatePresentingDocument={updatePresentingDocument}
>

View File

@@ -19,11 +19,7 @@ import React, {
useState,
} from "react";
import ReactMarkdown from "react-markdown";
import {
OnyxDocument,
FilteredOnyxDocument,
LoadedOnyxDocument,
} from "@/lib/search/interfaces";
import { OnyxDocument, FilteredOnyxDocument } from "@/lib/search/interfaces";
import { SearchSummary } from "./SearchSummary";
import { SkippedSearch } from "./SkippedSearch";
@@ -111,7 +107,6 @@ function FileDisplay({
<div key={file.id} className="w-fit">
<DocumentPreview
fileName={file.name || file.id}
maxWidth="max-w-64"
alignBubble={alignBubble}
/>
</div>
@@ -196,6 +191,7 @@ export const AIMessage = ({
onMessageSelection,
setPresentingDocument,
index,
toggledDocumentSidebar,
}: {
index?: number;
selectedMessageForDocDisplay?: number | null;
@@ -217,6 +213,7 @@ export const AIMessage = ({
citedDocuments?: [string, OnyxDocument][] | null;
toolCall?: ToolCallMetadata | null;
isComplete?: boolean;
toggledDocumentSidebar?: boolean;
hasDocs?: boolean;
handleFeedback?: (feedbackType: FeedbackType) => void;
handleShowRetrieved?: (messageNumber: number | null) => void;
@@ -339,6 +336,21 @@ export const AIMessage = ({
new Set((docs || []).map((doc) => doc.source_type))
).slice(0, 3);
const webSourceDomains: string[] = Array.from(
new Set(
docs
?.filter((doc) => doc.source_type === "web")
.map((doc) => {
try {
const url = new URL(doc.link);
return `https://${url.hostname}`;
} catch {
return doc.link; // fallback to full link if parsing fails
}
}) || []
)
);
const markdownComponents = useMemo(
() => ({
a: anchorCallback,
@@ -383,23 +395,24 @@ export const AIMessage = ({
<div
id="onyx-ai-message"
ref={trackedElementRef}
className={`py-5 ml-4 px-5 relative flex `}
className={`py-5 ml-4 lg:px-5 relative flex `}
>
<div
className={`mx-auto ${
shared ? "w-full" : "w-[90%]"
} max-w-message-max`}
>
<div className={`desktop:mr-12 ${!shared && "mobile:ml-0 md:ml-8"}`}>
<div className={`lg:mr-12 ${!shared && "mobile:ml-0 md:ml-8"}`}>
<div className="flex">
<AssistantIcon
size="small"
className="mobile:hidden"
size={24}
assistant={alternativeAssistant || currentPersona}
/>
<div className="w-full">
<div className="max-w-message-max break-words">
<div className="w-full ml-4">
<div className="w-full desktop:ml-4">
<div className="max-w-message-max break-words">
{!toolCall || toolCall.tool_name === SEARCH_TOOL_NAME ? (
<>
@@ -410,6 +423,8 @@ export const AIMessage = ({
query={query}
finished={toolCall?.tool_result != undefined}
handleSearchQueryEdit={handleSearchQueryEdit}
docs={docs || []}
toggleDocumentSelection={toggleDocumentSelection!}
/>
</div>
)}
@@ -465,14 +480,14 @@ export const AIMessage = ({
)}
{docs && docs.length > 0 && (
<div className="mt-2 -mx-8 w-full mb-4 flex relative">
<div className="mobile:hidden mt-2 -mx-8 w-full mb-4 flex relative">
<div className="w-full">
<div className="px-8 flex gap-x-2">
{!settings?.isMobile &&
docs.length > 0 &&
docs
.slice(0, 2)
.map((doc, ind) => (
.map((doc: OnyxDocument, ind: number) => (
<SourceCard
doc={doc}
key={ind}
@@ -482,13 +497,10 @@ export const AIMessage = ({
/>
))}
<SeeMoreBlock
documentSelectionToggled={
(documentSelectionToggled &&
selectedMessageForDocDisplay === messageId) ||
false
}
toggleDocumentSelection={toggleDocumentSelection}
toggled={toggledDocumentSidebar!}
toggleDocumentSelection={toggleDocumentSelection!}
uniqueSources={uniqueSources}
webSourceDomains={webSourceDomains}
/>
</div>
</div>
@@ -768,7 +780,7 @@ export const HumanMessage = ({
return (
<div
id="onyx-human-message"
className="pt-5 pb-1 px-2 lg:px-5 flex -mr-6 relative"
className="pt-5 pb-1 w-full lg:px-5 flex -mr-6 relative"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
@@ -778,7 +790,7 @@ export const HumanMessage = ({
} max-w-[790px]`}
>
<div className="xl:ml-8">
<div className="flex flex-col mr-4">
<div className="flex flex-col desktop:mr-4">
<FileDisplay alignBubble files={files || []} />
<div className="flex justify-end">
@@ -794,7 +806,6 @@ export const HumanMessage = ({
border
border-border
rounded-lg
bg-background-emphasis
pb-2
[&:has(textarea:focus)]::ring-1
[&:has(textarea:focus)]::ring-black
@@ -810,7 +821,6 @@ export const HumanMessage = ({
border-0
rounded-lg
overflow-y-hidden
bg-background-emphasis
whitespace-normal
break-word
overscroll-contain
@@ -820,6 +830,7 @@ export const HumanMessage = ({
text-text-editing-message
pl-4
overflow-y-auto
bg-background
pr-12
py-4`}
aria-multiline
@@ -893,7 +904,7 @@ export const HumanMessage = ({
</div>
) : typeof content === "string" ? (
<>
<div className="ml-auto mr-1 my-auto">
<div className="ml-auto flex items-center mr-1 h-fit my-auto">
{onEdit &&
isHovered &&
!isEditing &&

View File

@@ -4,12 +4,15 @@ import {
} from "@/components/BasicClickable";
import { HoverPopup } from "@/components/HoverPopup";
import { Hoverable } from "@/components/Hoverable";
import { SourceIcon } from "@/components/SourceIcon";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { OnyxDocument } from "@/lib/search/interfaces";
import { ValidSources } from "@/lib/types";
import { useEffect, useRef, useState } from "react";
import { FiCheck, FiEdit2, FiSearch, FiX } from "react-icons/fi";
@@ -45,11 +48,15 @@ export function SearchSummary({
query,
finished,
handleSearchQueryEdit,
docs,
toggleDocumentSelection,
}: {
index: number;
finished: boolean;
query: string;
handleSearchQueryEdit?: (query: string) => void;
docs: OnyxDocument[];
toggleDocumentSelection: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [finalQuery, setFinalQuery] = useState(query);
@@ -87,28 +94,64 @@ export function SearchSummary({
}, [query, isEditing]);
const searchingForDisplay = (
<div className={`flex p-1 rounded ${isOverflowed && "cursor-default"}`}>
<FiSearch className="flex-none mr-2 my-auto" size={14} />
<div className="flex flex-col gap-y-1">
<div
className={`${!finished && "loading-text"}
!text-sm !line-clamp-1 !break-all px-0.5`}
ref={searchingForRef}
className={`flex items-center w-full rounded ${
isOverflowed && "cursor-default"
}`}
>
{finished ? "Searched" : "Searching"} for:{" "}
<i>
{index === 1
? finalQuery.length > 50
? `${finalQuery.slice(0, 50)}...`
: finalQuery
: finalQuery}
</i>
<FiSearch className="mobile:hidden flex-none mr-2" size={14} />
<div
className={`${
!finished && "loading-text"
} text-xs desktop:text-sm mobile:ml-auto !line-clamp-1 !break-all px-0.5 flex-grow`}
ref={searchingForRef}
>
{finished ? "Searched" : "Searching"} for:{" "}
<i>
{index === 1
? finalQuery.length > 50
? `${finalQuery.slice(0, 50)}...`
: finalQuery
: finalQuery}
</i>
</div>
</div>
<div className="desktop:hidden">
{" "}
{docs && (
<button
className="cursor-pointer mr-2 flex items-center gap-0.5"
onClick={() => toggleDocumentSelection()}
>
{Array.from(new Set(docs.map((doc) => doc.source_type)))
.slice(0, 3)
.map((sourceType, idx) => (
<div key={idx} className="rounded-full">
<SourceIcon sourceType={sourceType} iconSize={14} />
</div>
))}
{Array.from(new Set(docs.map((doc) => doc.source_type))).length >
3 && (
<div className="rounded-full bg-gray-200 w-3.5 h-3.5 flex items-center justify-center">
<span className="text-[8px]">
+
{Array.from(new Set(docs.map((doc) => doc.source_type)))
.length - 3}
</span>
</div>
)}
<span className="text-xs underline">View sources</span>
</button>
)}
</div>
</div>
);
const editInput = handleSearchQueryEdit ? (
<div className="flex w-full mr-3">
<div className="my-2 w-full">
<div className="mobile:hidden flex w-full mr-3">
<div className="w-full">
<input
ref={editQueryRef}
value={finalQuery}
@@ -128,10 +171,10 @@ export function SearchSummary({
event.preventDefault();
}
}}
className="px-1 py-0.5 h-[28px] text-sm mr-2 w-full rounded-sm border border-border-strong"
className="px-1 py-0.5 h-[22px] text-sm mr-2 w-full rounded-sm border border-border-strong"
/>
</div>
<div className="ml-2 my-auto flex">
<div className="ml-2 -my-1 my-auto flex">
<Hoverable
icon={FiCheck}
onClick={() => {
@@ -155,12 +198,12 @@ export function SearchSummary({
) : null;
return (
<div className="flex">
<div className="flex items-center">
{isEditing ? (
editInput
) : (
<>
<div className="text-sm">
<div className="mobile:w-full mobile:mr-2 text-sm mobile:flex-grow">
{isOverflowed ? (
<HoverPopup
mainContent={searchingForDisplay}
@@ -176,12 +219,13 @@ export function SearchSummary({
searchingForDisplay
)}
</div>
{handleSearchQueryEdit && (
<TooltipProvider delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<button
className="my-auto hover:bg-hover p-1.5 rounded"
className="ml-2 -my-2 mobile:hidden hover:bg-hover p-1 rounded flex-shrink-0"
onClick={() => {
setIsEditing(true);
}}

View File

@@ -1,4 +1,5 @@
import { EmphasizedClickable } from "@/components/BasicClickable";
import { BasicClickable } from "@/components/BasicClickable";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { FiBook } from "react-icons/fi";
export function SkippedSearch({
@@ -7,22 +8,37 @@ export function SkippedSearch({
handleForceSearch: () => void;
}) {
return (
<div className="flex text-sm !pt-0 p-1">
<div className="flex mb-auto">
<FiBook className="my-auto flex-none mr-2" size={14} />
<div className="my-auto cursor-default">
<div className="flex w-full text-sm !pt-0 px-1">
<div className="flex w-full mb-auto">
<FiBook className="mobile:hidden my-auto flex-none mr-2" size={14} />
<div className="my-auto flex w-full items-center justify-between cursor-default">
<span className="mobile:hidden">
The AI decided this query didn&apos;t need a search
</span>
<span className="desktop:hidden">No search</span>
<p className="text-xs desktop:hidden">No search performed</p>
<CustomTooltip
content="Perform a search for this query"
showTick
line
wrap
>
<>
<BasicClickable
onClick={handleForceSearch}
className="ml-auto mr-4 -my-1 text-xs mobile:hidden bg-background/80 rounded-md px-2 py-1 cursor-pointer"
>
Force search?
</BasicClickable>
<button
onClick={handleForceSearch}
className="ml-auto mr-4 text-xs desktop:hidden underline-dotted decoration-dotted underline cursor-pointer"
>
Force search?
</button>
</>
</CustomTooltip>
</div>
</div>
<div className="ml-auto my-auto" onClick={handleForceSearch}>
<EmphasizedClickable size="sm">
<div className="w-24 text-xs">Force Search</div>
</EmphasizedClickable>
</div>
</div>
);
}

View File

@@ -60,7 +60,7 @@ export const FeedbackModal = ({
{feedbackType === "like" ? (
<FilledLikeIcon
size={20}
className="text-green-500 my-auto mr-2"
className="text-green-600 my-auto mr-2"
/>
) : (
<FilledLikeIcon
@@ -76,8 +76,8 @@ export const FeedbackModal = ({
{predefinedFeedbackOptions.map((feedback, index) => (
<button
key={index}
className={`bg-border hover:bg-hover text-default py-2 px-4 rounded m-1
${predefinedFeedback === feedback && "ring-2 ring-accent"}`}
className={`bg-background-dark hover:bg-hover text-default py-2 px-4 rounded m-1
${predefinedFeedback === feedback && "ring-2 ring-accent/20"}`}
onClick={() => handlePredefinedFeedback(feedback)}
>
{feedback}

View File

Some files were not shown because too many files have changed in this diff Show More