mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-09 08:52:42 +00:00
Compare commits
86 Commits
edge
...
new_ux_bra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0857a995fb | ||
|
|
4f05466fd8 | ||
|
|
9071d11295 | ||
|
|
49a26025c1 | ||
|
|
4593c782f6 | ||
|
|
f3404be997 | ||
|
|
30dc3cce9f | ||
|
|
e9313287cf | ||
|
|
7d721748e5 | ||
|
|
7453da8143 | ||
|
|
6584fba1cd | ||
|
|
bd8681b4f5 | ||
|
|
2e146f762b | ||
|
|
73698da6e2 | ||
|
|
6e26e0b528 | ||
|
|
a58c32b9dd | ||
|
|
89a44e680a | ||
|
|
3fd8da649f | ||
|
|
3571a1cc5d | ||
|
|
5ea65031c9 | ||
|
|
7f7e4fe19b | ||
|
|
a392f219b9 | ||
|
|
5d0866c990 | ||
|
|
6e99805224 | ||
|
|
5509d1cbb6 | ||
|
|
df9470c571 | ||
|
|
1bbcb9cbb6 | ||
|
|
07c2acadad | ||
|
|
a72d5d8600 | ||
|
|
a570022a73 | ||
|
|
d9bd26be2a | ||
|
|
883baad035 | ||
|
|
db0960e2ad | ||
|
|
8c0773a6d4 | ||
|
|
375affe39e | ||
|
|
8974562a5e | ||
|
|
28a2a2b8fd | ||
|
|
4dbbffc511 | ||
|
|
2946640bdc | ||
|
|
459073d4af | ||
|
|
40f9e2b2d3 | ||
|
|
21343dfbeb | ||
|
|
ae425fd92e | ||
|
|
28b0db978f | ||
|
|
09ee866d35 | ||
|
|
7b5e281c00 | ||
|
|
2d617467c0 | ||
|
|
6e8da44073 | ||
|
|
c256934b3b | ||
|
|
a09f9d27e2 | ||
|
|
79f987376e | ||
|
|
5e6e1893c0 | ||
|
|
a5d374d885 | ||
|
|
4b88869400 | ||
|
|
ded1c6972a | ||
|
|
f4d238a80e | ||
|
|
76f2b1890a | ||
|
|
a87f09d0db | ||
|
|
95e533e257 | ||
|
|
9b6963b877 | ||
|
|
ea393cca7e | ||
|
|
4d5125758a | ||
|
|
d081e7e550 | ||
|
|
b8303e2ce8 | ||
|
|
f6e8900622 | ||
|
|
65ecad7177 | ||
|
|
08b646baae | ||
|
|
08ce6b0cff | ||
|
|
1dcf0e87d4 | ||
|
|
01f1981842 | ||
|
|
b4df187cc0 | ||
|
|
84e0e2773f | ||
|
|
b74070dbb2 | ||
|
|
1d1ada371e | ||
|
|
7e26defdb0 | ||
|
|
5d572307d1 | ||
|
|
c9fa092a1a | ||
|
|
c81e0ff294 | ||
|
|
b850d0b23c | ||
|
|
322ca224b1 | ||
|
|
bf66795f57 | ||
|
|
9e3a2e1e91 | ||
|
|
f68cbdf879 | ||
|
|
c025ebbc08 | ||
|
|
1552b2694f | ||
|
|
7e8c21c807 |
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
536
backend/chatt.txt
Normal 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"
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
262
backend/onyx/db/input_prompt.py
Normal file
262
backend/onyx/db/input_prompt.py
Normal 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()
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
24
backend/onyx/seeding/input_prompts.yaml
Normal file
24
backend/onyx/seeding/input_prompts.yaml
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
156
backend/onyx/server/features/input_prompt/api.py
Normal file
156
backend/onyx/server/features/input_prompt/api.py
Normal 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
|
||||
47
backend/onyx/server/features/input_prompt/models.py
Normal file
47
backend/onyx/server/features/input_prompt/models.py
Normal 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,
|
||||
)
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
),
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
6
node_modules/.package-lock.json
generated
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "danswer",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
183
package-lock.json
generated
Normal file
183
package-lock.json
generated
Normal 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
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"react-datepicker": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-datepicker": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -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
848
web/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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
5
web/public/logo.svg
Normal 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
7
web/public/web.svg
Normal 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 |
@@ -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
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
@@ -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 "title" for this
|
||||
Starter Message. For example, "Write an
|
||||
email."
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Layout } from "@/components/admin/Layout";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
328
web/src/app/assistants/mine/AssistantCard.tsx
Normal file
328
web/src/app/assistants/mine/AssistantCard.tsx
Normal 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;
|
||||
237
web/src/app/assistants/mine/AssistantModal.tsx
Normal file
237
web/src/app/assistants/mine/AssistantModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
213
web/src/app/assistants/mine/AssistantSharingPopover.tsx
Normal file
213
web/src/app/assistants/mine/AssistantSharingPopover.tsx
Normal 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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
198
web/src/app/assistants/mine/AssistantVisibilityPopover.tsx
Normal file
198
web/src/app/assistants/mine/AssistantVisibilityPopover.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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've created that aren'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
31
web/src/app/assistants/mine/DeleteAssistantPopover.tsx
Normal file
31
web/src/app/assistants/mine/DeleteAssistantPopover.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
69
web/src/app/assistants/mine/MakePublicAssistantPopover.tsx
Normal file
69
web/src/app/assistants/mine/MakePublicAssistantPopover.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
106
web/src/app/auth/login/LoginPage.tsx
Normal file
106
web/src/app/auth/login/LoginPage.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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";
|
||||
166
web/src/app/chat/documentSidebar/DocumentResults.tsx
Normal file
166
web/src/app/chat/documentSidebar/DocumentResults.tsx
Normal 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";
|
||||
@@ -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
|
||||
"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
274
web/src/app/chat/folders/FolderDropdown.tsx
Normal file
274
web/src/app/chat/folders/FolderDropdown.tsx
Normal 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";
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
313
web/src/app/chat/input-prompts/InputPrompts.tsx
Normal file
313
web/src/app/chat/input-prompts/InputPrompts.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
web/src/app/chat/input-prompts/page.tsx
Normal file
15
web/src/app/chat/input-prompts/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
153
web/src/app/chat/input/LLMPopover.tsx
Normal file
153
web/src/app/chat/input/LLMPopover.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
244
web/src/app/chat/input/SimplifiedChatInputBar.tsx
Normal file
244
web/src/app/chat/input/SimplifiedChatInputBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
66
web/src/app/chat/layout.tsx
Normal file
66
web/src/app/chat/layout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
0
web/src/app/chat/modal/Inpu
Normal file
0
web/src/app/chat/modal/Inpu
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user