1
0
forked from github/onyx

Compare commits

...

36 Commits
main ... new_ux

Author SHA1 Message Date
pablodanswer
18794aeca0 ensure pushed 2025-01-10 14:04:45 -08:00
pablodanswer
919296b717 k 2025-01-10 12:06:51 -08:00
pablodanswer
5b9d2ee322 nit 2025-01-10 12:05:05 -08:00
pablodanswer
f34c8bdc71 quick nit 2025-01-10 12:04:20 -08:00
pablodanswer
de988149a3 quick nits 2025-01-10 12:01:06 -08:00
pablodanswer
309eeeb90c proper icons 2025-01-10 11:57:03 -08:00
pablodanswer
8e9bfaa280 commit 2025-01-09 23:24:07 -08:00
pablodanswer
d88d74d48b spacing 2025-01-09 21:04:44 -08:00
pablodanswer
7dd6eb9cb0 search related cleanup 2025-01-09 19:57:40 -08:00
pablodanswer
a8f90a824f fully functional 2025-01-09 19:54:15 -08:00
pablodanswer
2e163eb1c9 validate 2025-01-09 19:11:20 -08:00
pablodanswer
195fa58b49 quick sidebar cleanup 2025-01-09 17:16:27 -08:00
pablodanswer
20f48894d5 multitude of assorted updates 2025-01-09 15:14:25 -08:00
pablodanswer
bf5312862a clean 2025-01-09 14:11:26 -08:00
pablodanswer
c1c1da2736 quick updates 2025-01-09 10:47:25 -08:00
pablodanswer
a301d0e728 improved sidebar 2025-01-08 19:25:19 -08:00
pablodanswer
c0b303b445 nits 2025-01-08 18:19:49 -08:00
pablodanswer
4042493db4 decent visual for assistant modal 2025-01-08 17:32:21 -08:00
pablodanswer
5858a682c0 v1 assistant modal 2025-01-08 16:50:06 -08:00
pablodanswer
cd79614343 minor nit 2025-01-08 15:20:31 -08:00
pablodanswer
aa1791d3c5 v1 assistant editor 2025-01-08 15:07:54 -08:00
pablodanswer
9f90cce49b nit 2025-01-08 13:26:09 -08:00
pablodanswer
f281f1f861 push minor changes 2025-01-08 13:21:12 -08:00
pablodanswer
83b7ae436a nit 2025-01-08 10:54:15 -08:00
pablodanswer
e5451ea853 minor nit 2025-01-08 10:22:49 -08:00
pablodanswer
db4e7667b6 quick nits 2025-01-08 10:11:16 -08:00
pablodanswer
f16f1237ab v3 2025-01-07 17:00:35 -08:00
pablodanswer
b61854732b quick nits 2025-01-07 16:47:27 -08:00
pablodanswer
b46135f5d3 validate 2025-01-07 16:22:50 -08:00
pablodanswer
46dbca9f72 nit 2025-01-07 14:48:07 -08:00
pablodanswer
2e6b399e41 quick addition 2025-01-07 14:45:33 -08:00
pablodanswer
0b0f0ea13c organize components 2025-01-07 14:37:36 -08:00
pablodanswer
8e265c4c17 quick nit 2025-01-07 14:19:19 -08:00
pablodanswer
9d240cd0a6 k 2025-01-07 14:14:56 -08:00
pablodanswer
0d52160a6f v2 2025-01-07 14:14:56 -08:00
pablodanswer
bbe9e9db74 add chrome extension
minor clean up

additional handling

post rebase fixes

nit

quick bump

finalize

minor cleanup

organizational

Revert changes in backend directory

Revert changes in deployment directory

push misc changes

improve shortcut display + general nrf page layout

minor clean up

quick nit

update chrome

k

build fix

k

update

k
2025-01-07 14:14:45 -08:00
125 changed files with 6268 additions and 3486 deletions

View File

@@ -0,0 +1,26 @@
"""add pinned assistants
Revision ID: aeda5f2df4f6
Revises: 2955778aa44c
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 = "2955778aa44c"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"user", sa.Column("pinned_assistants", postgresql.JSONB(), nullable=True)
)
def downgrade() -> None:
op.drop_column("user", "pinned_assistants")

View File

@@ -162,6 +162,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

View File

@@ -69,3 +69,6 @@ def get_vespa_http_client(no_timeout: bool = False, http2: bool = True) -> httpx
timeout=None if no_timeout else VESPA_REQUEST_TIMEOUT,
http2=http2,
)
# from onyx.document_index.vespa.shared_utils.utils import get_vespa_http_client

View File

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

View File

@@ -47,6 +47,7 @@ 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
class UserInfo(BaseModel):

View File

@@ -673,6 +673,47 @@ def update_user_default_model(
db_session.commit()
class PinnedAssistantsRequest(BaseModel):
assistant_id: int
@router.patch("/user/pinned-assistants/{assistant_id}")
def update_user_pinned_assistants(
assistant_id: int,
pinned: 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)
pinned_assistants = no_auth_user.preferences.pinned_assistants or []
if pinned and assistant_id not in pinned_assistants:
pinned_assistants.append(assistant_id)
elif not pinned and assistant_id in pinned_assistants:
pinned_assistants.remove(assistant_id)
no_auth_user.preferences.pinned_assistants = pinned_assistants
set_no_auth_user_preferences(store, no_auth_user.preferences)
return
else:
raise RuntimeError("This should never happen")
pinned_assistants = UserInfo.from_model(user).preferences.pinned_assistants or []
if pinned:
if assistant_id not in pinned_assistants:
pinned_assistants.append(assistant_id)
else:
if assistant_id in pinned_assistants:
pinned_assistants.remove(assistant_id)
db_session.execute(
update(User)
.where(User.id == user.id) # type: ignore
.values(pinned_assistants=pinned_assistants)
)
db_session.commit()
class ChosenAssistantsRequest(BaseModel):
chosen_assistants: list[int]

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

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

183
package-lock.json generated Normal file
View File

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

8
package.json Normal file
View File

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

View File

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

345
web/package-lock.json generated
View File

@@ -17,7 +17,9 @@
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@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 +52,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 +80,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 +1198,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 +1226,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",
@@ -2912,6 +2932,85 @@
}
}
},
"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-popover": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
@@ -3063,6 +3162,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 +4944,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 +5038,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",
@@ -14008,6 +14332,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",

View File

@@ -19,7 +19,9 @@
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@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 +54,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 +82,7 @@
"devDependencies": {
"@chromatic-com/playwright": "^0.10.0",
"@tailwindcss/typography": "^0.5.10",
"@types/chrome": "^0.0.287",
"chromatic": "^11.18.1",
"eslint": "^8.48.0",
"eslint-config-next": "^14.1.0",

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

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

After

Width:  |  Height:  |  Size: 466 B

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

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

After

Width:  |  Height:  |  Size: 369 B

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -9,22 +9,36 @@ import {
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useEffect } from "react";
import { FiInfo, FiTrash2, FiPlus } from "react-icons/fi";
import { useEffect, useState } from "react";
import { FiInfo, FiTrash2, FiPlus, FiRefreshCcw } from "react-icons/fi";
import { StarterMessage } from "./interfaces";
import { Label } from "@/components/admin/connectors/Field";
import { Label, TextFormField } from "@/components/admin/connectors/Field";
import { Button } from "@/components/ui/button";
import { SwapIcon } from "@/components/icons/icons";
export default function StarterMessagesList({
values,
arrayHelpers,
isRefreshing,
touchStarterMessages,
debouncedRefreshPrompts,
autoStarterMessageEnabled,
errors,
setFieldValue,
}: {
values: StarterMessage[];
arrayHelpers: ArrayHelpers;
isRefreshing: boolean;
touchStarterMessages: () => void;
debouncedRefreshPrompts: (
values: StarterMessage[],
setFieldValue: any
) => void;
autoStarterMessageEnabled: boolean;
errors: any;
setFieldValue: any;
}) {
const [tooltipOpen, setTooltipOpen] = useState(false);
const { handleChange } = useFormikContext();
// Group starter messages into rows of 2 for display purposes
@@ -40,11 +54,11 @@ export default function StarterMessagesList({
<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">
<div className="grid grid-cols-2 gap-6 w-full max-w-4xl">
{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"
className="bg-white/90 w-full border border-border rounded-lg shadow-md transition-shadow duration-200 p-4"
>
<div className="space-y-5">
{isRefreshing ? (
@@ -66,93 +80,28 @@ export default function StarterMessagesList({
</div>
) : (
<>
<div>
<div className="flex w-full items-center gap-x-1">
<Label
small
className="text-sm font-medium text-gray-700"
>
Name
</Label>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
Shows up as the &quot;title&quot; for this
Starter Message. For example, &quot;Write an
email.&quot;
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Field
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
className="mt-1 w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
autoComplete="off"
placeholder="Enter a name..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<ErrorMessage
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<div>
<div className="flex w-full items-center gap-x-1">
<Label
small
className="text-sm font-medium text-gray-700"
>
Message
</Label>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
The actual message to be sent as the initial
user message.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Field
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
className="mt-1 text-sm w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition min-h-[100px] resize-y"
as="textarea"
autoComplete="off"
placeholder="Enter the message..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<ErrorMessage
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<TextFormField
label="Name"
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
placeholder="Enter a name..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<TextFormField
label="Message"
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
placeholder="Enter the message..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
</>
)}
</div>
@@ -174,25 +123,69 @@ export default function StarterMessagesList({
</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="relative gap-x-2 flex w-fit">
<TooltipProvider delayDuration={50}>
<Tooltip onOpenChange={setTooltipOpen} open={tooltipOpen}>
<TooltipTrigger asChild>
<Button
type="button"
size="sm"
onMouseEnter={() => setTooltipOpen(true)}
onMouseLeave={() => setTooltipOpen(false)}
onClick={() => debouncedRefreshPrompts(values, setFieldValue)}
className={`
${
isRefreshing || !autoStarterMessageEnabled
? "bg-neutral-200 border border-neutral-900 text-neutral-900 cursor-not-allowed"
: "bg-blue-500 text-white hover:bg-blue-600 active:bg-blue-700"
}
`}
>
<div className="flex text-xs items-center gap-x-2">
{isRefreshing ? (
<FiRefreshCcw className="w-4 h-4 animate-spin text-black" />
) : (
<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>
) : (
<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>
)}
</Tooltip>
</TooltipProvider>
{canAddMore && (
<Button
type="button"
className="text-xs w-fit"
onClick={() => {
arrayHelpers.push({
name: "",
message: "",
});
arrayHelpers.push({
name: "",
message: "",
});
}}
>
<FiPlus size={8} />
<span>Add Row</span>
</Button>
)}
</div>
</div>
);
}

View File

@@ -29,18 +29,13 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
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>
</>
);
}

View File

@@ -31,10 +31,6 @@ export default async function Page() {
return (
<div className="w-full">
<BackButton />
<AdminPageTitle
title="Create a New Assistant"
icon={<RobotIcon size={32} />}
/>
{body}
</div>
);

View File

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

View File

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

View File

@@ -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";
@@ -118,7 +118,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 +143,7 @@ export function AnonymousUserPath({
className="h-10 px-4"
onClick={() => {
navigator.clipboard.writeText(
`${NEXT_PUBLIC_CLOUD_DOMAIN}/anonymous/${anonymousUserPath}`
`${NEXT_PUBLIC_WEB_DOMAIN}/anonymous/${anonymousUserPath}`
);
setPopup({
message: "Invite link copied!",

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
"use client";
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
import { ChatSession } from "@/app/chat/interfaces";
import { Folder } from "@/app/chat/folders/interfaces";
import { User } from "@/lib/types";
import Cookies from "js-cookie";
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
import {
@@ -24,32 +20,20 @@ import { useChatContext } from "@/components/context/ChatContext";
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,
@@ -72,7 +56,6 @@ export default function SidebarWrapper<T extends object>({
mobile: settings?.isMobile,
});
const innerSidebarElementRef = useRef<HTMLDivElement>(null);
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -113,22 +96,10 @@ export default function SidebarWrapper<T extends object>({
: "opacity-0 w-[200px] pointer-events-none -translate-x-10"
}`}
>
<div className="w-full relative">
<HistorySidebar
page={page}
explicitlyUntoggle={explicitlyUntoggle}
ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
existingChats={chatSessions}
currentChatSession={null}
folders={folders}
openedFolders={openedFolders}
/>
</div>
<div className="w-full relative"></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}

View File

@@ -48,19 +48,5 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
);
}
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>
);
return <div>{body}</div>;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,255 @@
import React, { useCallback, useState } from "react";
import { Persona } from "@/app/admin/assistants/interfaces";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { OnyxIcon, PinnedIcon } from "@/components/icons/icons";
import { FaHashtag } from "react-icons/fa";
import {
FiShare2,
FiEye,
FiEyeOff,
FiTrash,
FiMoreHorizontal,
FiEdit,
} from "react-icons/fi";
import { toggleAssistantPinnedStatus } from "@/lib/assistants/updateAssistantPreferences";
import { useAssistants } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider";
import { useRouter } from "next/navigation";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { AssistantSharingModal } from "../mine/AssistantSharingModal";
import {
togglePersonaPublicStatus,
deletePersona,
} from "@/app/admin/assistants/lib";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantModal";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
export const AssistantBadge = ({ text }: { text: string }) => {
return (
<div className="h-4 px-1.5 py-1 bg-[#e6e3dd]/50 rounded-lg justify-center items-center gap-2.5 inline-flex">
<div className="text-[#4a4a4a] text-[10px] font-normal leading-[8px]">
{text}
</div>
</div>
);
};
const NewAssistantCard: React.FC<{
persona: Persona;
pinned: boolean;
closeModal: () => void;
}> = ({ persona, pinned, closeModal }) => {
const { user, refreshUser } = useUser();
const router = useRouter();
const { refreshAssistants } = useAssistants();
const [showSharingModal, setShowSharingModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showPublicModal, setShowPublicModal] = useState(false);
const isOwnedByUser = checkUserOwnsAssistant(user, persona);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const closePopover = useCallback(() => {
setIsPopoverOpen(false);
}, []);
const handleShare = () => {
setShowSharingModal(true);
closePopover();
};
const handleToggleVisibility = () => {
setShowPublicModal(true);
closePopover();
};
const handleDelete = () => {
setShowDeleteModal(true);
closePopover();
};
const handleEdit = () => {
router.push(`/assistants/edit/${persona.id}`);
closePopover();
};
return (
<div className="w-full p-2 overflow-visible bg-[#fefcf9] rounded shadow-[0px_0px_4px_0px_rgba(0,0,0,0.25)] flex">
<div className="ml-2 mr-4 mt-1 w-8 h-8">
<AssistantIcon assistant={persona} size="large" />
</div>
<div className="flex-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 text-base lg-normal">
{persona.name}
</h3>
<AssistantBadge text={persona.is_public ? "Public" : "Private"} />
</div>
{pinned && <span className="text-[#6c6c6c] h-0 text-sm">Pinned</span>}
</div>
<p className="text-black text-sm mb-1 line-clamp-2 h-[2.7em]">
{persona.description || "\u00A0"}
</p>
<div className="mb-1">
{persona.tools.length > 0 ? (
<>
<span className="text-black text-sm mr-1">Tools</span>
{persona.tools.map((tool, index) => (
<AssistantBadge key={index} text={tool.name} />
))}
</>
) : (
<AssistantBadge text="No Tools" />
)}
</div>
<div className="mb-1 flex flex-wrap">
{persona.document_sets.slice(0, 5).map((set, index) => (
<AssistantBadge key={index} text={set.name} />
))}
</div>
<div className="flex items-center justify-between mt-2">
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => {
router.push(`/chat?assistantId=${persona.id}`);
closeModal();
}}
className="hover:bg-neutral-100 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(persona.id, !pinned);
await refreshUser();
}}
className="hover:bg-neutral-100 px-2 py-1 gap-x-1 rounded border border-black flex items-center"
>
<PinnedIcon size={12} />
<span className="text-xs">{pinned ? "Unpin" : "Pin"}</span>
</button>
</TooltipTrigger>
<TooltipContent>
{pinned ? "Remove from" : "Add to"} your pinned list
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{isOwnedByUser && (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="hover:bg-neutral-100 p-1 rounded-full"
>
<FiMoreHorizontal size={16} />
</button>
</PopoverTrigger>
<PopoverContent className="z-[10000] w-40 p-2">
<button
onClick={handleShare}
className="w-full text-left flex items-center px-2 py-1 hover:bg-neutral-100 rounded"
>
<FiShare2 size={12} className="inline mr-2" />
Share
</button>
<button
onClick={handleToggleVisibility}
className="w-full text-left flex items-center px-2 py-1 hover:bg-neutral-100 rounded"
>
{persona.is_public ? (
<FiEyeOff size={12} className="inline mr-2" />
) : (
<FiEye size={12} className="inline mr-2" />
)}
Make {persona.is_public ? "Private" : "Public"}
</button>
<button
onClick={handleDelete}
className="w-full text-left items-center px-2 py-1 hover:bg-neutral-100 rounded text-red-600"
>
<FiTrash size={12} className="inline mr-2" />
Delete
</button>
<button
onClick={handleEdit}
className="w-full flex items-center text-left px-2 py-1 hover:bg-neutral-100 rounded"
>
<FiEdit size={12} className="inline mr-2" />
Edit
</button>
</PopoverContent>
</Popover>
)}
</div>
</div>
{showSharingModal && (
<AssistantSharingModal
assistant={persona}
user={user}
allUsers={[]}
onClose={() => {
setShowSharingModal(false);
refreshAssistants();
}}
show={showSharingModal}
/>
)}
{showDeleteModal && (
<DeleteEntityModal
entityType="Assistant"
entityName={persona.name}
onClose={() => setShowDeleteModal(false)}
onSubmit={async () => {
const success = await deletePersona(persona.id);
if (success) {
await refreshAssistants();
}
}}
/>
)}
{showPublicModal && (
<MakePublicAssistantModal
isPublic={persona.is_public}
onClose={() => setShowPublicModal(false)}
onShare={async (newPublicStatus: boolean) => {
await togglePersonaPublicStatus(persona.id, newPublicStatus);
await refreshAssistants();
}}
/>
)}
</div>
);
};
export default NewAssistantCard;

View File

@@ -0,0 +1,188 @@
"use client";
import React, { useMemo, useState } from "react";
import { Persona } from "@/app/admin/assistants/interfaces";
import { useRouter } from "next/navigation";
import { Modal } from "@/components/Modal";
import NewAssistantCard from "./AssistantCard";
import { useAssistants } from "@/components/context/AssistantsContext";
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 {
AdminCreated = "Admin-created",
Pinned = "Pinned",
Private = "Private",
Public = "Public",
Builtin = "Builtin",
}
const useAssistantFilter = () => {
const [assistantFilters, setAssistantFilters] = useState<
Record<AssistantFilter, boolean>
>({
[AssistantFilter.Builtin]: false,
[AssistantFilter.AdminCreated]: false,
[AssistantFilter.Pinned]: false,
[AssistantFilter.Private]: false,
[AssistantFilter.Public]: false,
});
const toggleAssistantFilter = (filter: AssistantFilter) => {
setAssistantFilters((prevFilters) => ({
...prevFilters,
[filter]: !prevFilters[filter],
}));
};
return { assistantFilters, toggleAssistantFilter };
};
export default function AssistantModal({
hideModal,
}: {
hideModal: () => void;
}) {
const { assistants, visibleAssistants, pinnedAssistants } = useAssistants();
const { assistantFilters, toggleAssistantFilter } = useAssistantFilter();
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const memoizedCurrentlyVisibleAssistants = useMemo(() => {
return assistants.filter((assistant) => {
const nameMatches = assistant.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);
const adminCreatedFilter =
!assistantFilters[AssistantFilter.AdminCreated] ||
assistant.is_default_persona;
const builtinFilter =
!assistantFilters[AssistantFilter.Builtin] || assistant.builtin_persona;
return (
nameMatches &&
publicFilter &&
privateFilter &&
pinnedFilter &&
adminCreatedFilter &&
builtinFilter
);
});
}, [assistants, searchQuery, assistantFilters, pinnedAssistants]);
return (
<Modal
hideCloseButton
onOutsideClick={hideModal}
className="max-w-4xl w-[95%] h-[80vh]"
>
<>
<div className="flex justify-between items-center mb-0">
<div className="h-10 px-4 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
<div className="h-16 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-[#dcdad4] flex items-center px-3">
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
type="text"
className="w-full h-full bg-transparent outline-none text-black"
/>
<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>
</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="ml-4 flex py-2 items-center gap-x-2">
<AssistantBadgeSelector
text="Public"
selected={assistantFilters[AssistantFilter.Public] ?? false}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Public)}
/>
<AssistantBadgeSelector
text="Private"
selected={assistantFilters[AssistantFilter.Private] ?? false}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Private)}
/>
<AssistantBadgeSelector
text="Admin-Created"
selected={assistantFilters[AssistantFilter.AdminCreated] ?? false}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.AdminCreated)
}
/>
<AssistantBadgeSelector
text="Pinned"
selected={assistantFilters[AssistantFilter.Pinned] ?? false}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Pinned)}
/>
<AssistantBadgeSelector
text="Builtin"
selected={assistantFilters[AssistantFilter.Builtin] ?? false}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Builtin)}
/>
</div>
<div className="w-full mt-2 justify-start h-fit px-2 grid grid-cols-1 md:grid-cols-2 gap-x-2 gap-y-3">
{memoizedCurrentlyVisibleAssistants.map((assistant, index) => (
<div key={index}>
<NewAssistantCard
pinned={pinnedAssistants.includes(assistant)}
persona={assistant}
closeModal={hideModal}
/>
</div>
))}
</div>
</>
</Modal>
);
}

View File

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

View File

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

View File

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

View File

@@ -18,10 +18,10 @@ export default async function Page() {
);
} else {
body = (
<div className="w-full my-16">
<div className="w-full py-16">
<div className="px-32">
<div className="mx-auto container">
<CardSection>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
defaultPublic={false}
@@ -35,21 +35,5 @@ export default async function Page() {
);
}
return (
<div>
<HeaderWrapper>
<div className="h-full flex flex-col">
<div className="flex my-auto">
<LargeBackButton />
<h1 className="flex text-xl text-strong font-bold my-auto">
New Assistant
</h1>
</div>
</div>
</HeaderWrapper>
{body}
</div>
);
return <div>{body}</div>;
}

View File

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

View File

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

View File

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

View File

@@ -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-normal font-['KH Teka TRIAL'] leading-normal">
{selectedPersona.description}
</p>
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { redirect, useRouter, useSearchParams } from "next/navigation";
import {
BackendChatSession,
BackendMessage,
@@ -47,20 +47,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";
@@ -105,12 +104,15 @@ 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";
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,
@@ -192,13 +201,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 +213,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 +296,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 +323,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 +404,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 +459,6 @@ export function ChatPage({
}
return;
}
setIsReady(true);
const shouldScrollToBottom =
visibleRange.get(existingChatSessionId) === undefined ||
visibleRange.get(existingChatSessionId)?.end == 0;
@@ -535,7 +537,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 +653,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 +800,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,
@@ -997,12 +1012,32 @@ export function ChatPage({
}
}, [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 +1113,7 @@ export function ChatPage({
alternativeAssistantOverride = null,
modelOverRide,
regenerationRequest,
overrideFileDescriptors,
}: {
messageIdToResend?: number;
messageOverride?: string;
@@ -1087,6 +1123,7 @@ export function ChatPage({
alternativeAssistantOverride?: Persona | null;
modelOverRide?: LlmOverride;
regenerationRequest?: RegenerationRequest | null;
overrideFileDescriptors?: FileDescriptor[];
} = {}) => {
let frozenSessionId = currentSessionId();
updateCanContinue(false, frozenSessionId);
@@ -1113,6 +1150,7 @@ export function ChatPage({
let currChatSessionId: string;
const isNewSession = chatSessionIdRef.current === null;
const searchParamBasedChatSessionName =
searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null;
@@ -1228,7 +1266,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 +1567,7 @@ export function ChatPage({
setSelectedMessageForDocDisplay(finalMessage.message_id);
}
setAlternativeGeneratingAssistant(null);
setSubmittedMessage("");
};
const onFeedback = async (
@@ -1815,6 +1854,7 @@ export function ChatPage({
end: 0,
mostVisibleMessageId: null,
};
useSendMessageToParent();
useEffect(() => {
if (noAssistants) {
@@ -1889,6 +1929,7 @@ export function ChatPage({
handleSlackChatRedirect();
}, [searchParams, router]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
@@ -1918,25 +1959,13 @@ export function ChatPage({
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 +1986,10 @@ export function ChatPage({
});
};
}
if (!user) {
redirect("/auth/login");
}
if (noAssistants)
return (
<>
@@ -2039,16 +2072,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);
}}
@@ -2130,6 +2162,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">
@@ -2154,6 +2190,8 @@ export function ChatPage({
>
<div className="w-full relative">
<HistorySidebar
setShowAssistantsModal={setShowAssistantsModal}
assistants={assistants}
explicitlyUntoggle={explicitlyUntoggle}
stopGenerating={stopGenerating}
reset={() => setMessage("")}
@@ -2162,6 +2200,7 @@ export function ChatPage({
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
backgroundToggled={toggledSidebar || showHistorySidebar}
currentAssistantId={liveAssistant?.id}
existingChats={chatSessions}
currentChatSession={selectedChatSession}
folders={folders}
@@ -2197,16 +2236,13 @@ export function ChatPage({
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
`}
>
<ChatFilters
<DocumentResults
setPresentingDocument={setPresentingDocument}
modal={false}
filterManager={filterManager}
ccPairs={ccPairs}
tags={tags}
documentSets={documentSets}
ref={innerSidebarElementRef}
showFilters={filtersToggled}
closeSidebar={() => setDocumentSidebarToggled(false)}
closeSidebar={() =>
setTimeout(() => setDocumentSidebarToggled(false), 300)
}
selectedMessage={aiMessage}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
@@ -2241,13 +2277,16 @@ export function ChatPage({
}
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 +2314,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 +2331,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 +2351,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")
}
>
@@ -2548,6 +2541,7 @@ export function ChatPage({
) {
toggleDocumentSidebar();
}
setSelectedMessageForDocDisplay(
message.messageId
);
@@ -2774,19 +2768,20 @@ export function ChatPage({
</div>
)}
<ChatInputBar
toggleDocumentSidebar={toggleDocumentSidebar}
availableSources={sources}
availableDocumentSets={documentSets}
availableTags={tags}
filterManager={filterManager}
llmOverrideManager={llmOverrideManager}
removeDocs={() => {
clearSelectedDocuments();
}}
showDocs={() => {
setFiltersToggled(false);
setDocumentSidebarToggled(true);
}}
showConfigureAPIKey={() =>
setShowApiKeyModal(true)
}
chatState={currentSessionChatState}
stopGenerating={stopGenerating}
openModelSettings={() => setSettingsToggled(true)}
selectedDocuments={selectedDocuments}
// assistant stuff
selectedAssistant={liveAssistant}
@@ -2796,12 +2791,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 +2822,6 @@ export function ChatPage({
</div>
</div>
</div>
{!settings?.isMobile && (
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
flex-none
overflow-y-hidden
transition-all
duration-300
ease-in-out
${
documentSidebarToggled && retrievalEnabled
? "w-[400px]"
: "w-[0px]"
}
`}
></div>
)}
</div>
)}
</Dropzone>

View File

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

View File

@@ -4,14 +4,20 @@ import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper";
export default function WrappedChat({
initiallyToggled,
firstMessage,
}: {
initiallyToggled: boolean;
firstMessage?: string;
}) {
return (
<FunctionalWrapper
initiallyToggled={initiallyToggled}
content={(toggledSidebar, toggle) => (
<ChatPage toggle={toggle} toggledSidebar={toggledSidebar} />
<ChatPage
toggle={toggle}
toggledSidebar={toggledSidebar}
firstMessage={firstMessage}
/>
)}
/>
);

View File

@@ -77,18 +77,23 @@ 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-background-dark/50 hover:bg-[#ebe7de]/80"
}`}
>
<button
onClick={() => openDocument(document, setPresentingDocument)}
className="cursor-pointer text-left flex flex-col px-2 py-1.5"
className="cursor-pointer text-left flex flex-col"
>
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
{document.is_internet || document.source_type === "web" ? (

View File

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

View File

@@ -0,0 +1,180 @@
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(() => {
if (isOpen) {
document.addEventListener("mousedown", handleOutsideClick);
} else {
document.removeEventListener("mousedown", handleOutsideClick);
}
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
};
}, [isOpen]);
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
className={`fixed inset-0 bg-[#2f291d] bg-opacity-10 cursor-pointer transition-opacity duration-300 ${
isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
}`}
onClick={closeSidebar}
/>
<div
id="onyx-chat-sidebar"
className={`relative rounded-t-xl rounded-l-3xl -mb-8 bg-background-sidebar 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-start gap-x-2">
<SourcesIcon size={32} />
<h2 className="text-xl font-bold text-text-900">Sources</h2>
</div>
<div className="border-b border-divider-history-sidebar-bar mx-3" />
<div className="overflow-y-auto h-fit mb-8 pb-8 -mx-1 sm:mx-0 flex-grow gap-y-0 default-scrollbar dark-scrollbar flex flex-col">
{dedupedDocuments.length > 0 ? (
dedupedDocuments.map((document, ind) => (
<div key={document.document_id} className="w-full">
<ChatDocumentDisplay
setPresentingDocument={setPresentingDocument}
closeSidebar={closeSidebar}
modal={modal}
document={document}
isSelected={selectedDocumentIds.includes(
document.document_id
)}
handleSelect={(documentId) => {
toggleDocumentSelection(
dedupedDocuments.find(
(doc) => doc.document_id === documentId
)!
);
}}
hideSelection={isSharedChat}
tokenLimitReached={tokenLimitReached}
/>
</div>
))
) : (
<div className="mx-3" />
)}
</div>
</div>
<div
className={`sticky bottom-4 w-full left-0 flex justify-center transition-opacity duration-300 ${
hasSelectedDocuments
? "opacity-100"
: "opacity-0 pointer-events-none"
}`}
>
<button
className="text-sm font-medium py-2 px-4 rounded-full transition-colors bg-neutral-900 text-white"
onClick={clearSelectedDocuments}
>
{`Remove ${
delayedSelectedDocumentCount > 0
? delayedSelectedDocumentCount
: ""
} Source${delayedSelectedDocumentCount > 1 ? "s" : ""}`}
</button>
</div>
</div>
</div>
</>
);
}
);
DocumentResults.displayName = "DocumentResults";

View File

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

View File

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

View File

@@ -0,0 +1,148 @@
import React, { useState, useRef, useEffect, ReactNode } from "react";
import { Folder } from "./interfaces";
import { ChatSession } from "../interfaces";
import { ChatSessionDisplay } from "../sessionSidebar/ChatSessionDisplay";
import {
FiChevronDown,
FiChevronRight,
FiEdit,
FiTrash2,
FiCheck,
FiX,
} from "react-icons/fi";
import { Caret } from "@/components/icons/icons";
import { addChatToFolder } from "./FolderManagement";
import { FaPencilAlt } from "react-icons/fa";
import { Pencil } from "@phosphor-icons/react";
import { PencilIcon } from "lucide-react";
interface FolderDropdownProps {
folder:
| Folder
| {
folder_name: "Chats";
chat_sessions: ChatSession[];
folder_id?: "chats";
};
currentChatId?: string;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
closeSidebar?: () => void;
onEdit?: (folderId: number | "chats", newName: string) => void;
onDelete?: (folderId: number | "chats") => void;
onDrop?: (folderId: number, chatSessionId: string) => void;
children?: ReactNode;
}
export const FolderDropdown: React.FC<FolderDropdownProps> = ({
folder,
currentChatId,
showShareModal,
showDeleteModal,
closeSidebar,
onEdit,
onDelete,
onDrop,
children,
}) => {
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);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
const handleEdit = () => {
setIsEditing(true);
};
const handleSave = () => {
if (onEdit && folder.folder_id) {
onEdit(folder.folder_id, newFolderName);
}
setIsEditing(false);
};
const handleCancel = () => {
setNewFolderName(folder.folder_name);
setIsEditing(false);
};
const handleDelete = () => {
if (onDelete && folder.folder_id) {
onDelete(folder.folder_id);
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const chatSessionId = e.dataTransfer.getData("text/plain");
if (folder.folder_id && folder.folder_id !== "chats" && onDrop) {
onDrop(folder.folder_id, chatSessionId);
}
};
return (
<div className="mb-2" onDragOver={handleDragOver} onDrop={handleDrop}>
<div
className="flex items-center w-full text-[#6c6c6c] rounded-md p-1 relative"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<button
className="flex items-center flex-grow"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? (
<Caret size={16} className="mr-1" />
) : (
<Caret size={16} className="-rotate-90 mr-1" />
)}
{isEditing ? (
<input
ref={inputRef}
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
className="text-sm font-medium bg-transparent border-none outline-none"
/>
) : (
<div className="flex items-center">
<span className="text-sm font-medium">{folder.folder_name}</span>
{isHovered && folder.folder_id !== "chats" && (
<button onClick={handleEdit} className="ml-1 px-1">
<PencilIcon size={14} />
</button>
)}
</div>
)}
</button>
{isHovered && !isEditing && folder.folder_id !== "chats" && (
<button onClick={handleDelete} className="px-1 ml-auto">
<FiTrash2 size={14} />
</button>
)}
{isEditing && (
<div className="-my-1">
<button onClick={handleSave} className="p-1 text-green-500">
<FiCheck size={14} />
</button>
<button onClick={handleCancel} className="p-1 text-red-500">
<FiX size={14} />
</button>
</div>
)}
</div>
{isOpen && <div className="mr-4 ml-1 mt-1">{children}</div>}
</div>
);
};

View File

@@ -296,7 +296,7 @@ const FolderItem = ({
{/* Expanded Folder Content */}
{isExpanded && folders && (
<div className={"ml-2 pl-2 border-l border-border"}>
<div className={"mr-4 pl-2 border-l border-border"}>
{folders.map((chatSession) => (
<ChatSessionDisplay
key={chatSession.id}

View File

@@ -1,9 +1,17 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi";
import {
FiPlusCircle,
FiPlus,
FiInfo,
FiX,
FiSearch,
FiFilter,
} from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption";
import { Persona } from "@/app/admin/assistants/interfaces";
import LLMPopover from "./LLMPopover";
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";
@@ -12,8 +20,9 @@ import {
InputBarPreviewImageProvider,
} from "../files/InputBarPreview";
import {
AssistantsIconSkeleton,
DocumentIcon2,
FileIcon,
OnyxIcon,
SendIcon,
StopGeneratingIcon,
} from "@/components/icons/icons";
@@ -26,50 +35,95 @@ 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 { getDateRangeString } from "@/lib/dateUtils";
const MAX_INPUT_HEIGHT = 200;
export const SourceChip = ({
icon,
title,
onRemove,
onClick,
}: {
icon: React.ReactNode;
title: string;
onRemove: () => void;
onClick?: () => void;
}) => (
<div
onClick={onClick ? onClick : undefined}
className={`
flex-none
flex
items-center
px-2
bg-hover
text-sm
border
gap-x-1.5
border-border
rounded-md
box-border
gap-x-1
h-8
${onClick ? "cursor-pointer" : ""}
`}
>
{icon}
{title}
<XIcon
size={16}
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[];
}
export function ChatInputBar({
removeDocs,
openModelSettings,
showDocs,
toggleDocumentSidebar,
filterManager,
showConfigureAPIKey,
selectedDocuments,
message,
setMessage,
stopGenerating,
onSubmit,
filterManager,
chatState,
// assistants
@@ -81,7 +135,10 @@ export function ChatInputBar({
handleFileUpload,
textAreaRef,
alternativeAssistant,
toggleFilters,
availableSources,
availableDocumentSets,
availableTags,
llmOverrideManager,
}: ChatInputBarProps) {
useEffect(() => {
const textarea = textAreaRef.current;
@@ -111,11 +168,9 @@ export function ChatInputBar({
}
};
const settings = useContext(SettingsContext);
const { finalAssistants: assistantOptions } = useAssistants();
const { llmProviders } = useChatContext();
const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null);
const suggestionsRef = useRef<HTMLDivElement | null>(null);
const [showSuggestions, setShowSuggestions] = useState(false);
@@ -219,7 +274,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 +286,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 inset-x-0 top-0 w-full 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-medium shadow-lg mx-2 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 line-clamp-1">
{currentAssistant.id == selectedAssistant.id &&
"(default) "}
{currentAssistant.description}
@@ -257,8 +315,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} />
@@ -275,23 +334,22 @@ export function ChatInputBar({
opacity-100
w-full
h-fit
bg-bl
flex
bg-background
flex-col
border
border-[#E5E7EB]
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 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 +380,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 +392,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,13 +408,12 @@ export function ChatInputBar({
resize-none
px-5
py-4
h-14
`}
autoFocus
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder="Ask me anything.."
placeholder="Ask me anything..."
value={message}
onKeyDown={(event) => {
if (
@@ -425,7 +430,111 @@ 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
key="time-range"
icon={<CalendarIcon size={16} />}
title={`${getDateRangeString(
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 ? (
<InputBarPreviewImageProvider
key={`file-${index}`}
file={file}
onDelete={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
isUploading={file.isUploading || false}
/>
) : (
<InputBarPreview
key={`file-${index}`}
file={file}
onDelete={() => {
setFiles(
files.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
isUploading={file.isUploading || false}
/>
)
)}
</div>
</div>
)}
<div className="flex items-center space-x-1 mr-12 px-4 pb-2">
<ChatInputOption
flexPriority="stiff"
name="File"
@@ -433,7 +542,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 +553,30 @@ export function ChatInputBar({
};
input.click();
}}
tooltipContent={"Upload files"}
/>
{toggleFilters && (
<ChatInputOption
flexPriority="stiff"
name="Filters"
Icon={FiSearch}
onClick={toggleFilters}
/>
)}
{(filterManager.selectedSources.length > 0 ||
filterManager.selectedDocumentSets.length > 0 ||
filterManager.selectedTags.length > 0 ||
filterManager.timeRange) &&
toggleFilters && (
<FiltersDisplay
filterManager={filterManager}
toggleFilters={toggleFilters}
<FilterPopup
availableSources={availableSources}
availableDocumentSets={availableDocumentSets}
availableTags={availableTags}
filterManager={filterManager}
trigger={
<ChatInputOption
flexPriority="stiff"
name="Filters"
Icon={FiFilter}
tooltipContent="Filter your search"
/>
)}
}
/>
<LLMPopover
llmProviders={llmProviders}
llmOverrideManager={llmOverrideManager}
requiresImageGeneration={false}
currentAssistant={selectedAssistant}
/>
</div>
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
@@ -495,7 +609,7 @@ export function ChatInputBar({
disabled={chatState != "input"}
>
<SendIcon
size={28}
size={26}
className={`text-emphasis text-white p-1 rounded-full ${
chatState == "input" && message
? "bg-submit-background"

View File

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

View File

@@ -0,0 +1,145 @@
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]
);
return (
<Popover>
<PopoverTrigger asChild>
<button className="focus:outline-none">
<ChatInputOption
toggle
flexPriority="stiff"
name="Models"
Icon={AnthropicSVG}
tooltipContent="Switch models"
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-64 p-1 bg-white 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))}
>
<div className="relative flex-shrink-0">
<div
className={`w-4 h-4 rounded-full border ${
currentLlm === name
? "border-gray-700"
: "border-gray-300"
}`}
>
{currentLlm === name && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-gray-700"></div>
</div>
)}
</div>
</div>
{icon({ size: 16, className: "flex-none my-auto " })}
<span className="line-clamp-1 ">
{getDisplayNameForModel(name)}
</span>
{(() => {
if (currentAssistant?.llm_model_version_override === name) {
return (
<span className="flex-none ml-auto text-xs">
(assistant)
</span>
);
} else if (globalDefault.modelName === name) {
return (
<span className="flex-none ml-auto text-xs">
(user default)
</span>
);
}
return null;
})()}
</button>
);
}
return null;
})}
</div>
</PopoverContent>
</Popover>
);
}

View File

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

View File

@@ -0,0 +1,62 @@
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,
defaultAssistantId,
shouldShowWelcomeModal,
ccPairs,
} = data;
return (
<>
<InstantSSRAutoRefresh />
<ChatProvider
value={{
chatSessions,
availableSources,
ccPairs,
documentSets,
tags,
availableDocumentSets: documentSets,
availableTags: tags,
llmProviders,
folders,
openedFolders,
shouldShowWelcomeModal,
defaultAssistantId,
}}
>
{children}
</ChatProvider>
</>
);
}

View File

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

View File

@@ -19,11 +19,7 @@ import React, {
useState,
} from "react";
import ReactMarkdown from "react-markdown";
import {
OnyxDocument,
FilteredOnyxDocument,
LoadedOnyxDocument,
} from "@/lib/search/interfaces";
import { OnyxDocument, FilteredOnyxDocument } from "@/lib/search/interfaces";
import { SearchSummary } from "./SearchSummary";
import { SkippedSearch } from "./SkippedSearch";
@@ -111,7 +107,6 @@ function FileDisplay({
<div key={file.id} className="w-fit">
<DocumentPreview
fileName={file.name || file.id}
maxWidth="max-w-64"
alignBubble={alignBubble}
/>
</div>
@@ -383,23 +378,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 +406,8 @@ export const AIMessage = ({
query={query}
finished={toolCall?.tool_result != undefined}
handleSearchQueryEdit={handleSearchQueryEdit}
docs={docs || []}
toggleDocumentSelection={toggleDocumentSelection!}
/>
</div>
)}
@@ -465,7 +463,7 @@ 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 &&
@@ -768,7 +766,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 +776,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 +792,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 +807,6 @@ export const HumanMessage = ({
border-0
rounded-lg
overflow-y-hidden
bg-background-emphasis
whitespace-normal
break-word
overscroll-contain
@@ -820,6 +816,7 @@ export const HumanMessage = ({
text-text-editing-message
pl-4
overflow-y-auto
bg-background
pr-12
py-4`}
aria-multiline
@@ -893,7 +890,7 @@ export const HumanMessage = ({
</div>
) : typeof content === "string" ? (
<>
<div className="ml-auto mr-1 my-auto">
<div className="ml-auto flex items-center mr-1 h-fit my-auto">
{onEdit &&
isHovered &&
!isEditing &&

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,384 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import { useUser } from "@/components/user/UserProvider";
import { usePopup } from "@/components/admin/connectors/Popup";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { v4 as uuidv4 } from "uuid";
import { Button } from "@/components/ui/button";
import { SimplifiedChatInputBar } from "../input/SimplifiedChatInputBar";
import { Menu } from "lucide-react";
import { Shortcut } from "./interfaces";
import {
MaxShortcutsReachedModal,
NewShortCutModal,
} from "@/components/extension/Shortcuts";
import { Modal } from "@/components/Modal";
import { useNightTime } from "@/lib/dateUtils";
import { useFilters } from "@/lib/hooks";
import { uploadFilesForChat } from "../lib";
import { ChatFileType, FileDescriptor } from "../interfaces";
import { useChatContext } from "@/components/context/ChatContext";
import Dropzone from "react-dropzone";
import { useSendMessageToParent } from "@/lib/extension/utils";
import { useNRFPreferences } from "@/components/context/NRFPreferencesContext";
import { SettingsPanel } from "../../components/nrf/SettingsPanel";
import { ShortcutsDisplay } from "../../components/nrf/ShortcutsDisplay";
import LoginPage from "../../auth/login/LoginPage";
import { AuthType, NEXT_PUBLIC_WEB_DOMAIN } from "@/lib/constants";
import { sendSetDefaultNewTabMessage } from "@/lib/extension/utils";
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import { CHROME_MESSAGE } from "@/lib/extension/constants";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
export default function NRFPage({
requestCookies,
}: {
requestCookies: ReadonlyRequestCookies;
}) {
const {
theme,
defaultLightBackgroundUrl,
defaultDarkBackgroundUrl,
shortcuts: shortCuts,
setShortcuts: setShortCuts,
setUseOnyxAsNewTab,
showShortcuts,
} = useNRFPreferences();
const filterManager = useFilters();
const { isNight } = useNightTime();
const { user } = useUser();
const { ccPairs, documentSets, tags, llmProviders } = useChatContext();
const { popup, setPopup } = usePopup();
// State
const [message, setMessage] = useState("");
const [settingsOpen, setSettingsOpen] = useState<boolean>(false);
const [editingShortcut, setEditingShortcut] = useState<Shortcut | null>(null);
const [backgroundUrl, setBackgroundUrl] = useState<string>(
theme === "light" ? defaultLightBackgroundUrl : defaultDarkBackgroundUrl
);
// Modals
const [showTurnOffModal, setShowTurnOffModal] = useState<boolean>(false);
const [showShortCutModal, setShowShortCutModal] = useState(false);
const [showMaxShortcutsModal, setShowMaxShortcutsModal] = useState(false);
const [showLoginModal, setShowLoginModal] = useState<boolean>(!user);
// Refs
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setBackgroundUrl(
theme === "light" ? defaultLightBackgroundUrl : defaultDarkBackgroundUrl
);
}, [theme, defaultLightBackgroundUrl, defaultDarkBackgroundUrl]);
useSendMessageToParent();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const toggleSettings = () => {
setSettingsOpen((prev) => !prev);
};
// If user toggles the "Use Onyx" switch to off, prompt a modal
const handleUseOnyxToggle = (checked: boolean) => {
if (!checked) {
setShowTurnOffModal(true);
} else {
setUseOnyxAsNewTab(true);
sendSetDefaultNewTabMessage(true);
}
};
const availableSources = ccPairs.map((ccPair) => ccPair.source);
const [currentMessageFiles, setCurrentMessageFiles] = useState<
FileDescriptor[]
>([]);
const handleImageUpload = async (acceptedFiles: File[]) => {
const tempFileDescriptors = acceptedFiles.map((file) => ({
id: uuidv4(),
type: file.type.startsWith("image/")
? ChatFileType.IMAGE
: ChatFileType.DOCUMENT,
isUploading: true,
}));
// only show loading spinner for reasonably large files
const totalSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
if (totalSize > 50 * 1024) {
setCurrentMessageFiles((prev) => [...prev, ...tempFileDescriptors]);
}
const removeTempFiles = (prev: FileDescriptor[]) => {
return prev.filter(
(file) => !tempFileDescriptors.some((newFile) => newFile.id === file.id)
);
};
await uploadFilesForChat(acceptedFiles).then(([files, error]) => {
if (error) {
setCurrentMessageFiles((prev) => removeTempFiles(prev));
setPopup({
type: "error",
message: error,
});
} else {
setCurrentMessageFiles((prev) => [...removeTempFiles(prev), ...files]);
}
});
};
const confirmTurnOff = () => {
setUseOnyxAsNewTab(false);
setShowTurnOffModal(false);
sendSetDefaultNewTabMessage(false);
};
// Auth related
const [authType, setAuthType] = useState<AuthType | null>(null);
const [fetchingAuth, setFetchingAuth] = useState(false);
useEffect(() => {
// If user is already logged in, no need to fetch auth data
if (user) return;
async function fetchAuthData() {
setFetchingAuth(true);
try {
const res = await fetch("/api/auth/type", {
method: "GET",
credentials: "include",
});
if (!res.ok) {
throw new Error(`Failed to fetch auth type: ${res.statusText}`);
}
const data = await res.json();
setAuthType(data.auth_type);
} catch (err) {
console.error("Error fetching auth data:", err);
} finally {
setFetchingAuth(false);
}
}
fetchAuthData();
}, [user]);
const onSubmit = async ({
messageOverride,
}: {
messageOverride?: string;
} = {}) => {
const userMessage = messageOverride || message;
let filterString = filterManager?.getFilterString();
if (currentMessageFiles.length > 0) {
filterString +=
"&files=" + encodeURIComponent(JSON.stringify(currentMessageFiles));
}
const newHref =
`${NEXT_PUBLIC_WEB_DOMAIN}/chat?send-on-load=true&user-prompt=` +
encodeURIComponent(userMessage) +
filterString;
if (typeof window !== "undefined" && window.parent) {
window.parent.postMessage(
{ type: CHROME_MESSAGE.LOAD_NEW_PAGE, href: newHref },
"*"
);
} else {
window.location.href = newHref;
}
};
return (
<div
className="relative w-full h-full flex flex-col min-h-screen bg-cover bg-center bg-no-repeat overflow-hidden transition-[background-image] duration-300 ease-in-out"
style={{
backgroundImage: `url(${backgroundUrl})`,
}}
>
<div className="absolute top-0 right-0 p-4 z-10">
<button
aria-label="Open settings"
onClick={toggleSettings}
className="bg-white bg-opacity-70 rounded-full p-2.5 cursor-pointer hover:bg-opacity-80 transition-colors duration-200"
>
<Menu size={12} className="text-neutral-900" />
</button>
</div>
<Dropzone onDrop={handleImageUpload} noClick>
{({ getRootProps }) => (
<div
{...getRootProps()}
className="absolute top-20 left-0 w-full h-full flex flex-col"
>
<div className="pointer-events-auto absolute top-[40%] left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-[90%] lg:max-w-3xl">
<h1
className={`pl-2 text-xl text-left w-full mb-4 ${
theme === "light" ? "text-neutral-800" : "text-white"
}`}
>
{isNight
? "End your day with Onyx"
: "Start your day with Onyx"}
</h1>
<SimplifiedChatInputBar
onSubmit={onSubmit}
handleFileUpload={handleImageUpload}
message={message}
setMessage={setMessage}
files={currentMessageFiles}
setFiles={setCurrentMessageFiles}
filterManager={filterManager}
textAreaRef={textAreaRef}
existingSources={availableSources}
availableDocumentSets={documentSets}
availableTags={tags}
/>
<ShortcutsDisplay
shortCuts={shortCuts}
showShortcuts={showShortcuts}
setEditingShortcut={setEditingShortcut}
setShowShortCutModal={setShowShortCutModal}
openShortCutModal={() => {
if (shortCuts.length >= 6) {
setShowMaxShortcutsModal(true);
} else {
setEditingShortcut(null);
setShowShortCutModal(true);
}
}}
/>
</div>
</div>
)}
</Dropzone>
{showMaxShortcutsModal && (
<MaxShortcutsReachedModal
onClose={() => setShowMaxShortcutsModal(false)}
/>
)}
{showShortCutModal && (
<NewShortCutModal
setPopup={setPopup}
onDelete={(shortcut: Shortcut) => {
setShortCuts(
shortCuts.filter((s: Shortcut) => s.name !== shortcut.name)
);
setShowShortCutModal(false);
}}
isOpen={showShortCutModal}
onClose={() => {
setEditingShortcut(null);
setShowShortCutModal(false);
}}
onAdd={(shortCut: Shortcut) => {
if (editingShortcut) {
setShortCuts(
shortCuts
.filter((s) => s.name !== editingShortcut.name)
.concat(shortCut)
);
} else {
setShortCuts([...shortCuts, shortCut]);
}
setShowShortCutModal(false);
}}
editingShortcut={editingShortcut}
/>
)}
<SettingsPanel
settingsOpen={settingsOpen}
toggleSettings={toggleSettings}
handleUseOnyxToggle={handleUseOnyxToggle}
/>
<Dialog open={showTurnOffModal} onOpenChange={setShowTurnOffModal}>
<DialogContent className="w-fit max-w-[95%]">
<DialogHeader>
<DialogTitle>Turn off Onyx new tab page?</DialogTitle>
<DialogDescription>
You&apos;ll see your browser&apos;s default new tab page instead.
<br />
You can turn it back on anytime in your Onyx settings.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 justify-center">
<Button
variant="outline"
onClick={() => setShowTurnOffModal(false)}
>
Cancel
</Button>
<Button variant="destructive" onClick={confirmTurnOff}>
Turn off
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{!user && authType !== "disabled" && showLoginModal ? (
<Modal className="max-w-md mx-auto">
{fetchingAuth ? (
<p className="p-4">Loading login info</p>
) : authType == "basic" ? (
<LoginPage
showPageRedirect
authUrl={null}
authTypeMetadata={{
authType: authType as AuthType,
autoRedirect: false,
requiresVerification: false,
anonymousUserEnabled: null,
}}
nextUrl="/nrf"
searchParams={{}}
/>
) : (
<div className="flex flex-col items-center">
<h2 className="text-center text-xl text-strong font-bold mb-4">
Welcome to Onyx
</h2>
<Button
className="bg-accent w-full hover:bg-accent-hover text-white"
onClick={() => {
if (window.top) {
window.top.location.href = "/auth/login";
} else {
window.location.href = "/auth/login";
}
}}
>
Log in
</Button>
</div>
)}
</Modal>
) : (
llmProviders.length == 0 && <ApiKeyModal setPopup={setPopup} />
)}
</div>
);
}

View File

@@ -0,0 +1,5 @@
export interface Shortcut {
name: string;
url: string;
favicon?: string;
}

View File

@@ -0,0 +1,20 @@
import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { cookies } from "next/headers";
import NRFPage from "./NRFPage";
import { NRFPreferencesProvider } from "../../../components/context/NRFPreferencesContext";
export default async function Page() {
noStore();
const requestCookies = await cookies();
return (
<div className="w-full h-full bg-black">
<InstantSSRAutoRefresh />
<NRFPreferencesProvider>
<NRFPage requestCookies={requestCookies} />
</NRFPreferencesProvider>
</div>
);
}

View File

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

View File

@@ -15,6 +15,7 @@ export const SEARCH_PARAM_NAMES = {
SUBMIT_ON_LOAD: "submit-on-load",
// chat title
TITLE: "title",
FILES: "files",
// for seeding chats
SEEDED: "seeded",
SEND_ON_LOAD: "send-on-load",

View File

@@ -0,0 +1,20 @@
import { useRouter } from "next/router";
import { ChatSession } from "../interfaces";
export const ChatGroup = ({
groupName,
toggled,
chatSessions,
}: {
groupName: string;
toggled: boolean;
chatSessions: ChatSession[];
}) => {
const router = useRouter();
return toggled ? (
<div>
<p>{groupName}</p>
</div>
) : null;
};

View File

@@ -22,6 +22,8 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
import { WarningCircle } from "@phosphor-icons/react";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { FaHashtag } from "react-icons/fa";
export function ChatSessionDisplay({
chatSession,
search,
@@ -48,20 +50,8 @@ export function ChatSessionDisplay({
useState(false);
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
const [chatName, setChatName] = useState(chatSession.name);
const [delayedSkipGradient, setDelayedSkipGradient] = useState(skipGradient);
const settings = useContext(SettingsContext);
useEffect(() => {
if (skipGradient) {
setDelayedSkipGradient(true);
} else {
const timer = setTimeout(() => {
setDelayedSkipGradient(skipGradient);
}, 300);
return () => clearTimeout(timer);
}
}, [skipGradient]);
const onRename = async () => {
const response = await renameChatSession(chatSession.id, chatName);
if (response.ok) {
@@ -92,7 +82,7 @@ export function ChatSessionDisplay({
)}
<Link
className="flex my-1 group relative"
className="flex group relative"
key={chatSession.id}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => {
@@ -122,9 +112,10 @@ export function ChatSessionDisplay({
);
}}
>
<BasicSelectable padding="extra" fullWidth selected={isSelected}>
<BasicSelectable fullWidth selected={isSelected}>
<>
<div className="flex relative">
<div className="flex relative gap-x-2">
<FaHashtag size={12} className="flex-none my-auto" />
{isRenamingChat ? (
<input
value={chatName}
@@ -145,7 +136,7 @@ export function ChatSessionDisplay({
${
isSelected
? "to-background-chat-selected"
: "group-hover:to-background-chat-hover"
: "group-hover:to-background-chat-hover to-background-sidebar"
} `}
/>
</p>

View File

@@ -1,9 +1,9 @@
"use client";
import { FiEdit, FiFolderPlus } from "react-icons/fi";
import { FiEdit, FiFolderPlus, FiMoreHorizontal } from "react-icons/fi";
import React, { ForwardedRef, forwardRef, useContext, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { ChatSession } from "../interfaces";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { Folder } from "../folders/interfaces";
@@ -11,10 +11,21 @@ import { createFolder } from "../folders/FolderManagement";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { AssistantsIconSkeleton } from "@/components/icons/icons";
import {
AssistantsIconSkeleton,
NewChatIcon,
OnyxIcon,
PinnedIcon,
PlusIcon,
} from "@/components/icons/icons";
import { PagesTab } from "./PagesTab";
import { pageType } from "./types";
import LogoWithText from "@/components/header/LogoWithText";
import { Persona } from "@/app/admin/assistants/interfaces";
import { FaSearch } from "react-icons/fa";
import { useAssistants } from "@/components/context/AssistantsContext";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { buildChatUrl } from "../lib";
interface HistorySidebarProps {
page: pageType;
@@ -32,16 +43,21 @@ interface HistorySidebarProps {
explicitlyUntoggle: () => void;
showDeleteAllModal?: () => void;
backgroundToggled?: boolean;
assistants: Persona[];
currentAssistantId?: number | null;
setShowAssistantsModal?: (show: boolean) => void;
}
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
(
{
reset = () => null,
setShowAssistantsModal = () => null,
toggled,
page,
existingChats,
currentChatSession,
assistants,
folders,
openedFolders,
explicitlyUntoggle,
@@ -52,9 +68,11 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
showDeleteModal,
showDeleteAllModal,
backgroundToggled,
currentAssistantId,
},
ref: ForwardedRef<HTMLDivElement>
) => {
const searchParams = useSearchParams();
const router = useRouter();
const { popup, setPopup } = usePopup();
@@ -62,6 +80,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
const [newFolderId, setNewFolderId] = useState<number | null>(null);
const currentChatId = currentChatSession?.id;
const { pinnedAssistants } = useAssistants();
// NOTE: do not do something like the below - assume that the parent
// will handle properly refreshing the existingChats
@@ -115,7 +134,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
{page == "chat" && (
<div className="mx-3 mt-4 gap-y-1 flex-col text-text-history-sidebar-button flex gap-x-1.5 items-center items-center">
<Link
className=" w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
className="w-full p-2 rounded items-center cursor-pointer transition-all duration-150 flex gap-x-2"
href={
`/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
@@ -132,43 +151,61 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
}
}}
>
<FiEdit className="flex-none text-text-history-sidebar-button" />
<p className="my-auto flex items-center text-sm">New Chat</p>
</Link>
<button
onClick={() =>
createFolder("New Folder")
.then((folderId) => {
router.refresh();
setNewFolderId(folderId);
})
.catch((error) => {
console.error("Failed to create folder:", error);
setPopup({
message: `Failed to create folder: ${error.message}`,
type: "error",
});
})
}
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-history-sidebar-button-hover cursor-pointer transition-all duration-150 flex gap-x-2"
>
<FiFolderPlus className="my-auto text-text-history-sidebar-button" />
<p className="my-auto flex items-center text-sm">New Folder</p>
</button>
<Link
href="/assistants/mine"
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-history-sidebar-button-hover cursor-pointer transition-all duration-150 flex gap-x-2"
>
<AssistantsIconSkeleton className="h-4 w-4 my-auto text-text-history-sidebar-button" />
<p className="my-auto flex items-center text-sm">
Manage Assistants
<NewChatIcon
size={20}
className="flex-none text-text-history-sidebar-button"
/>
<p className="my-auto flex items-center text-base">
Start New Chat
</p>
</Link>
</div>
)}
<div className="border-b border-divider-history-sidebar-bar pb-4 mx-3" />
<div className="my-2 mx-3">
<div className="flex text-sm gap-x-2 mx-2 items-center">
<PinnedIcon
className="text-text-history-sidebar-button"
size={12}
/>
Pinned
</div>
<div className="flex flex-col gap-y-1 mt-2">
{pinnedAssistants.slice(0, 3).map((assistant) => (
<button
onClick={() => {
router.push(buildChatUrl(searchParams, null, assistant.id));
// router.push(`/${page}?assistantId=${assistant.id}`);
}}
className={`cursor-pointer hover:bg-hover-light ${
currentAssistantId === assistant.id ? "bg-hover-light" : ""
} flex items-center gap-x-2 py-1 px-2 rounded-md`}
key={assistant.id}
>
<AssistantIcon
assistant={assistant}
size={16}
className="flex-none"
/>
<p className="text-base text-black">{assistant.name}</p>
{currentAssistantId === assistant.id && (
<PinnedIcon className="ml-auto text-text-dark" size={12} />
)}
</button>
))}
<button
onClick={() => setShowAssistantsModal(true)}
className="cursor-pointer hover:bg-hover-light flex items-center gap-x-2 py-1 px-2 rounded-md"
>
<FiMoreHorizontal size={16} className="flex-none" />
<p className="text-base text-black">More Assistants</p>
</button>
</div>
</div>
<PagesTab
setNewFolderId={setNewFolderId}
newFolderId={newFolderId}
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
@@ -177,7 +214,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
existingChats={existingChats}
currentChatId={currentChatId}
folders={folders}
openedFolders={openedFolders}
showDeleteAllModal={showDeleteAllModal}
/>
</div>

View File

@@ -1,166 +1,273 @@
import { ChatSession } from "../interfaces";
import { groupSessionsByDateRange } from "../lib";
import { ChatSessionDisplay } from "./ChatSessionDisplay";
import { removeChatFromFolder } from "../folders/FolderManagement";
import { FolderList } from "../folders/FolderList";
import {
createFolder,
updateFolderName,
deleteFolder,
addChatToFolder,
} from "../folders/FolderManagement";
import { Folder } from "../folders/interfaces";
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { pageType } from "./types";
import { FiTrash2 } from "react-icons/fi";
import { FiPlus, FiTrash2, FiEdit, FiCheck, FiX } from "react-icons/fi";
import { NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED } from "@/lib/constants";
import { FolderDropdown } from "../folders/FolderDropdown";
import { ChatSessionDisplay } from "./ChatSessionDisplay";
import { useState, useCallback, useRef } from "react";
export function PagesTab({
page,
existingChats,
currentChatId,
folders,
openedFolders,
closeSidebar,
newFolderId,
showShareModal,
showDeleteModal,
showDeleteAllModal,
setNewFolderId,
}: {
page: pageType;
existingChats?: ChatSession[];
currentChatId?: string;
folders?: Folder[];
openedFolders?: { [key: number]: boolean };
closeSidebar?: () => void;
newFolderId: number | null;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
showDeleteAllModal?: () => void;
setNewFolderId: (folderId: number) => void;
}) {
const groupedChatSessions = existingChats
? groupSessionsByDateRange(existingChats)
: [];
const { setPopup } = usePopup();
const router = useRouter();
const [isDragOver, setIsDragOver] = useState<boolean>(false);
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
const newFolderInputRef = useRef<HTMLInputElement>(null);
const handleDropToRemoveFromFolder = async (
event: React.DragEvent<HTMLDivElement>
) => {
event.preventDefault();
setIsDragOver(false); // Reset drag over state on drop
const chatSessionId = event.dataTransfer.getData(CHAT_SESSION_ID_KEY);
const folderId = event.dataTransfer.getData(FOLDER_ID_KEY);
if (folderId) {
try {
await removeChatFromFolder(parseInt(folderId, 10), chatSessionId);
router.refresh(); // Refresh the page to reflect the changes
} catch (error) {
setPopup({
message: "Failed to remove chat from folder",
type: "error",
const handleEditFolder = useCallback(
(folderId: number | "chats", newName: string) => {
if (folderId === "chats") return; // Don't edit the default "Chats" folder
updateFolderName(folderId, newName)
.then(() => {
router.refresh();
setPopup({
message: "Folder updated successfully",
type: "success",
});
})
.catch((error: Error) => {
console.error("Failed to update folder:", error);
setPopup({
message: `Failed to update folder: ${error.message}`,
type: "error",
});
});
},
[router, setPopup]
);
const handleDeleteFolder = useCallback(
(folderId: number | "chats") => {
if (folderId === "chats") return; // Don't delete the default "Chats" folder
if (
confirm(
"Are you sure you want to delete this folder? This action cannot be undone."
)
) {
deleteFolder(folderId)
.then(() => {
router.refresh();
setPopup({
message: "Folder deleted successfully",
type: "success",
});
})
.catch((error: Error) => {
console.error("Failed to delete folder:", error);
setPopup({
message: `Failed to delete folder: ${error.message}`,
type: "error",
});
});
}
}
},
[router, setPopup]
);
const handleCreateFolder = useCallback(() => {
setIsCreatingFolder(true);
setTimeout(() => {
newFolderInputRef.current?.focus();
}, 0);
}, []);
const handleNewFolderSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const newFolderName = newFolderInputRef.current?.value;
if (newFolderName) {
createFolder(newFolderName)
.then((folderId) => {
router.refresh();
setNewFolderId(folderId);
setPopup({
message: "Folder created successfully",
type: "success",
});
})
.catch((error) => {
console.error("Failed to create folder:", error);
setPopup({
message: `Failed to create folder: ${error.message}`,
type: "error",
});
});
}
setIsCreatingFolder(false);
},
[router, setNewFolderId, setPopup]
);
const ungroupedChats =
existingChats?.filter((chat) => chat.folder_id === null) || [];
const chatFolder: {
folder_name: "Chats";
chat_sessions: ChatSession[];
folder_id?: "chats";
} = {
folder_name: "Chats",
chat_sessions: ungroupedChats,
folder_id: "chats",
};
const isHistoryEmpty = !existingChats || existingChats.length === 0;
const handleDrop = useCallback(
async (folderId: number, chatSessionId: string) => {
try {
await addChatToFolder(folderId, chatSessionId);
router.refresh();
setPopup({
message: "Chat added to folder successfully",
type: "success",
});
} catch (error: unknown) {
console.error("Failed to add chat to folder:", error);
setPopup({
message: `Failed to add chat to folder: ${
error instanceof Error ? error.message : "Unknown error"
}`,
type: "error",
});
}
},
[router, setPopup]
);
const renderChatSession = useCallback(
(chat: ChatSession) => (
<div
key={chat.id}
draggable
onDragStart={(e) => {
e.dataTransfer.setData("text/plain", chat.id);
}}
>
<ChatSessionDisplay
chatSession={chat}
isSelected={currentChatId === chat.id}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
closeSidebar={closeSidebar}
/>
</div>
),
[currentChatId, showShareModal, showDeleteModal, closeSidebar]
);
return (
<div className="flex flex-col relative h-full overflow-y-auto mb-1 ml-3 miniscroll mobile:pb-40">
<div
className={` flex-grow overflow-y-auto ${
NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && "pb-20 "
className={`flex-grow overflow-y-auto ${
NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && "pb-20"
}`}
>
{folders && folders.length > 0 && (
<div className="py-2 border-b border-border">
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
Chat Folders
</div>
<FolderList
newFolderId={newFolderId}
folders={folders}
currentChatId={currentChatId}
openedFolders={openedFolders}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
/>
</div>
{!isHistoryEmpty && (
<FolderDropdown
folder={chatFolder}
currentChatId={currentChatId}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
closeSidebar={closeSidebar}
onEdit={handleEditFolder}
onDelete={handleDeleteFolder}
onDrop={handleDrop}
>
{chatFolder.chat_sessions.map(renderChatSession)}
</FolderDropdown>
)}
<div
onDragOver={(event) => {
event.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDropToRemoveFromFolder}
className={`pt-1 transition duration-300 ease-in-out mr-3 ${
isDragOver ? "bg-hover" : ""
} rounded-md`}
>
{(page == "chat" || page == "search") && (
<p className="my-2 text-xs text-sidebar-subtle flex font-bold">
{page == "chat" && "Chat "}
{page == "search" && "Search "}
History
</p>
)}
{isHistoryEmpty ? (
<p className="text-sm mt-2 w-[250px]">
Try sending a message! Your chat history will appear here.
</p>
) : (
Object.entries(groupedChatSessions).map(
([dateRange, chatSessions], ind) => {
if (chatSessions.length > 0) {
return (
<div key={dateRange}>
<div
className={`text-xs text-text-sidebar-subtle ${
ind != 0 && "mt-5"
} flex pb-0.5 mb-1.5 font-medium`}
>
{dateRange}
</div>
{chatSessions
.filter((chat) => chat.folder_id === null)
.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={`${chat.id}-${chat.name}`}>
<ChatSessionDisplay
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={closeSidebar}
search={page == "search"}
chatSession={chat}
isSelected={isSelected}
skipGradient={isDragOver}
/>
</div>
);
})}
</div>
);
}
}
)
)}
</div>
{showDeleteAllModal && NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && (
<div className="absolute w-full border-t border-t-border bg-background-100 bottom-0 left-0 p-4">
{folders &&
folders.length > 0 &&
folders
.sort((a, b) => a.display_priority - b.display_priority)
.map((folder) => (
<FolderDropdown
key={folder.folder_id}
folder={folder}
currentChatId={currentChatId}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
closeSidebar={closeSidebar}
onEdit={handleEditFolder}
onDelete={handleDeleteFolder}
onDrop={handleDrop}
>
{folder.chat_sessions.map(renderChatSession)}
</FolderDropdown>
))}
{isHistoryEmpty && (
<p className="text-sm mt-2 w-[250px]">
Try sending a message! Your chat history will appear here.
</p>
)}
{isCreatingFolder ? (
<form onSubmit={handleNewFolderSubmit} className="mt-2 relative">
<input
ref={newFolderInputRef}
type="text"
placeholder="Enter folder name"
className="w-full p-1 text-sm border rounded pr-8"
/>
<button
className="w-full py-2 px-4 text-text-600 hover:text-text-800 bg-background-125 border border-border-strong/50 shadow-sm rounded-md transition-colors duration-200 flex items-center justify-center text-sm"
onClick={showDeleteAllModal}
type="button"
onClick={() => setIsCreatingFolder(false)}
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
<FiTrash2 className="mr-2" size={14} />
Clear All History
<FiX size={16} />
</button>
</div>
</form>
) : (
<button
className="flex text-[#6c6c6c] gap-x-1 mt-2"
onClick={handleCreateFolder}
>
<FiPlus className="my-auto" />
<p className="my-auto flex items-center text-sm">Create Group</p>
</button>
)}
</div>
{showDeleteAllModal && NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && (
<div className="absolute w-full border-t border-t-border bg-background-100 bottom-0 left-0 p-4">
<button
className="w-full py-2 px-4 text-text-600 hover:text-text-800 bg-background-125 border border-border-strong/50 shadow-sm rounded-md transition-colors duration-200 flex items-center justify-center text-sm"
onClick={showDeleteAllModal}
>
<FiTrash2 className="mr-2" size={14} />
Clear All History
</button>
</div>
)}
</div>
);
}

View File

@@ -20,7 +20,7 @@ import { Persona } from "@/app/admin/assistants/interfaces";
import { Button } from "@/components/ui/button";
import { OnyxDocument } from "@/lib/search/interfaces";
import TextView from "@/components/chat_search/TextView";
import { ChatFilters } from "../../documentSidebar/ChatFilters";
import { DocumentResults } from "../../documentSidebar/DocumentResults";
import { Modal } from "@/components/Modal";
import FunctionalHeader from "@/components/chat_search/Header";
import FixedLogo from "../../shared_chat_search/FixedLogo";
@@ -107,7 +107,7 @@ export function SharedChatDisplay({
{documentSidebarToggled && settings?.isMobile && (
<div className="md:hidden">
<Modal noPadding noScroll>
<ChatFilters
<DocumentResults
isSharedChat={true}
selectedMessage={
selectedMessageForDocDisplay
@@ -128,10 +128,6 @@ export function SharedChatDisplay({
isOpen={true}
setPresentingDocument={setPresentingDocument}
modal={true}
ccPairs={[]}
tags={[]}
documentSets={[]}
showFilters={false}
closeSidebar={() => {
setDocumentSidebarToggled(false);
}}
@@ -166,7 +162,7 @@ export function SharedChatDisplay({
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
`}
>
<ChatFilters
<DocumentResults
modal={false}
isSharedChat={true}
selectedMessage={
@@ -186,10 +182,6 @@ export function SharedChatDisplay({
initialWidth={400}
isOpen={true}
setPresentingDocument={setPresentingDocument}
ccPairs={[]}
tags={[]}
documentSets={[]}
showFilters={false}
closeSidebar={() => {
setDocumentSidebarToggled(false);
}}
@@ -199,7 +191,6 @@ export function SharedChatDisplay({
)}
<div className="flex mobile:hidden max-h-full overflow-hidden ">
<FunctionalHeader
documentSidebarToggled={documentSidebarToggled}
sidebarToggled={false}
toggleSidebar={() => {}}
page="chat"

View File

@@ -0,0 +1,179 @@
import React from "react";
import { Switch } from "@/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { useNRFPreferences } from "../../../components/context/NRFPreferencesContext";
import {
darkExtensionImages,
lightExtensionImages,
} from "@/lib/extension/constants";
const SidebarSwitch = ({
checked,
onCheckedChange,
label,
}: {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label: string;
}) => (
<div className="flex justify-between items-center py-2">
<span className="text-sm text-gray-300">{label}</span>
<Switch
checked={checked}
onCheckedChange={onCheckedChange}
className="data-[state=checked]:bg-white data-[state=checked]:border-neutral-200 data-[state=unchecked]:bg-gray-600"
circleClassName="data-[state=checked]:bg-neutral-200"
/>
</div>
);
const RadioOption = ({
value,
label,
description,
groupValue,
onChange,
}: {
value: string;
label: string;
description: string;
groupValue: string;
onChange: (value: string) => void;
}) => (
<div className="flex items-start space-x-2 mb-2">
<RadioGroupItem
value={value}
id={value}
className="mt-1 border border-gray-600 data-[state=checked]:border-white data-[state=checked]:bg-white"
/>
<Label htmlFor={value} className="flex flex-col">
<span className="text-sm text-gray-300">{label}</span>
{description && (
<span className="text-xs text-gray-500">{description}</span>
)}
</Label>
</div>
);
export const SettingsPanel = ({
settingsOpen,
toggleSettings,
handleUseOnyxToggle,
}: {
settingsOpen: boolean;
toggleSettings: () => void;
handleUseOnyxToggle: (checked: boolean) => void;
}) => {
const {
theme,
setTheme,
defaultLightBackgroundUrl,
setDefaultLightBackgroundUrl,
defaultDarkBackgroundUrl,
setDefaultDarkBackgroundUrl,
useOnyxAsNewTab,
showShortcuts,
setShowShortcuts,
} = useNRFPreferences();
const toggleTheme = (newTheme: string) => {
setTheme(newTheme);
};
const updateBackgroundUrl = (url: string) => {
if (theme === "light") {
setDefaultLightBackgroundUrl(url);
} else {
setDefaultDarkBackgroundUrl(url);
}
};
return (
<div
className="fixed top-0 right-0 w-[360px] h-full bg-[#202124] text-gray-300 overflow-y-auto z-20 transition-transform duration-300 ease-in-out transform"
style={{
transform: settingsOpen ? "translateX(0)" : "translateX(100%)",
boxShadow: "-2px 0 10px rgba(0,0,0,0.3)",
}}
>
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-white">
Home page settings
</h2>
<button
aria-label="Close"
onClick={toggleSettings}
className="text-gray-400 hover:text-white"
>
</button>
</div>
<h3 className="text-sm font-semibold mb-2">General</h3>
<SidebarSwitch
checked={useOnyxAsNewTab}
onCheckedChange={handleUseOnyxToggle}
label="Use Onyx as new tab page"
/>
<SidebarSwitch
checked={showShortcuts}
onCheckedChange={setShowShortcuts}
label="Show bookmarks"
/>
<h3 className="text-sm font-semibold mt-6 mb-2">Theme</h3>
<RadioGroup
value={theme}
onValueChange={toggleTheme}
className="space-y-2"
>
<RadioOption
value="light"
label="Light theme"
description="Light theme"
groupValue={theme}
onChange={toggleTheme}
/>
<RadioOption
value="dark"
label="Dark theme"
description="Dark theme"
groupValue={theme}
onChange={toggleTheme}
/>
</RadioGroup>
<h3 className="text-sm font-semibold mt-6 mb-2">Background</h3>
<div className="grid grid-cols-4 gap-2">
{(theme === "dark" ? darkExtensionImages : lightExtensionImages).map(
(bg: string, index: number) => (
<div
key={bg}
onClick={() => updateBackgroundUrl(bg)}
className={`relative ${
index === 0 ? "col-span-2 row-span-2" : ""
} cursor-pointer rounded-sm overflow-hidden`}
style={{
paddingBottom: index === 0 ? "100%" : "50%",
}}
>
<div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: `url(${bg})` }}
/>
{(theme === "light"
? defaultLightBackgroundUrl
: defaultDarkBackgroundUrl) === bg && (
<div className="absolute inset-0 border-2 border-blue-400 rounded" />
)}
</div>
)
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,46 @@
"use client";
import React from "react";
import { ShortCut, AddShortCut } from "@/components/extension/Shortcuts";
import { Shortcut } from "@/app/chat/nrf/interfaces";
interface ShortcutsDisplayProps {
shortCuts: Shortcut[];
showShortcuts: boolean;
setEditingShortcut: (shortcut: Shortcut | null) => void;
setShowShortCutModal: (show: boolean) => void;
openShortCutModal: () => void;
}
export const ShortcutsDisplay: React.FC<ShortcutsDisplayProps> = ({
shortCuts,
showShortcuts,
setEditingShortcut,
setShowShortCutModal,
openShortCutModal,
}) => {
return (
<div
className={`
mx-auto flex flex-wrap justify-center gap-x-6 gap-y-4 mt-12
transition-all duration-700 ease-in-out
${
showShortcuts
? "opacity-100 max-h-[500px]"
: "opacity-0 max-h-0 overflow-hidden pointer-events-none"
}
`}
>
{shortCuts.map((shortCut: Shortcut, index: number) => (
<ShortCut
key={index}
onEdit={() => {
setEditingShortcut(shortCut);
setShowShortCutModal(true);
}}
shortCut={shortCut}
/>
))}
<AddShortCut openShortCutModal={openShortCutModal} />
</div>
);
};

0
web/src/app/ee/Hori Normal file
View File

View File

@@ -10,7 +10,7 @@ export default function WrappedAssistantsStats({
assistantId: number;
}) {
return (
<SidebarWrapper page="chat" initiallyToggled={initiallyToggled}>
<SidebarWrapper initiallyToggled={initiallyToggled}>
<AssistantStats assistantId={assistantId} />
</SidebarWrapper>
);

View File

@@ -53,7 +53,9 @@
--text-950: #0a0a0a;
/* solidDark - Nearly pure black */
--background: #fafafa;
--background: #fefcfa;
/* --background: #fafafa; */
/* 50 - Almost white */
--background-100: #f5f5f5;
/* neutral-100 - Very light gray */
@@ -99,7 +101,7 @@
/* blue-500 - Bright blue */
--link-hover: #1d4ed8;
/* blue-700 - Dark blue */
--error: #ef4444;
--error: #f87171;
/* red-500 - Bright red */
--undo: #ef4444;
/* red-500 - Bright red */
@@ -142,7 +144,7 @@
/* Light pink for non-selectable elements */
--highlight-text: #fef9c3;
/* Very light yellow for highlighted text */
--user-bubble: #f1f2f4;
--user-bubble: #f1eee8;
/* near gray-100 - Very light grayish */
--ai-bubble: #272a2d;
/* Dark grayish for AI bubbles */

View File

@@ -13,7 +13,6 @@ import { Metadata } from "next";
import { buildClientUrl } from "@/lib/utilsSS";
import { Inter } from "next/font/google";
import { EnterpriseSettings, GatingType } from "./admin/settings/interfaces";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
import { AppProvider } from "@/components/context/AppProvider";
import { PHProvider } from "./providers";
@@ -23,6 +22,7 @@ import { Suspense } from "react";
import PostHogPageView from "./PostHogPageView";
import Script from "next/script";
import { LogoType } from "@/components/logo/Logo";
import { Hanken_Grotesk } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
@@ -30,6 +30,12 @@ const inter = Inter({
display: "swap",
});
const hankenGrotesk = Hanken_Grotesk({
subsets: ["latin"],
variable: "--font-hanken-grotesk",
display: "swap",
});
export async function generateMetadata(): Promise<Metadata> {
let logoLocation = buildClientUrl("/onyx.ico");
let enterpriseSettings: EnterpriseSettings | null = null;
@@ -67,7 +73,7 @@ export default async function RootLayout({
combinedSettings?.settings.product_gating ?? GatingType.NONE;
const getPageContent = (content: React.ReactNode) => (
<html lang="en" className={`${inter.variable} font-sans`}>
<html lang="en" className={`${inter.variable} ${hankenGrotesk.variable}`}>
<head>
<meta
name="viewport"
@@ -99,7 +105,9 @@ export default async function RootLayout({
/>
)}
</head>
<body className={`relative ${inter.variable} font-sans`}>
<body
className={`relative ${inter.variable} caret-[#8A1FCD] font-hanken`}
>
<div
className={`text-default min-h-screen bg-background ${
process.env.THEME_IS_DARK?.toLowerCase() === "true" ? "dark" : ""

View File

@@ -32,7 +32,7 @@ export const HoverableIcon: React.FC<{
}> = ({ icon, onClick }) => {
return (
<div
className="hover:bg-hover p-1.5 rounded h-fit cursor-pointer"
className="hover:bg-background-chat-hover p-1.5 rounded h-fit cursor-pointer"
onClick={onClick}
>
{icon}

View File

@@ -8,7 +8,7 @@ export function OnyxInitializingLoader() {
return (
<div className="mx-auto my-auto animate-pulse">
<Logo height={96} width={96} className="mx-auto mb-3" />
<p className="text-lg font-bold">
<p className="text-lg text-text font-semibold">
Initializing {settings?.enterpriseSettings?.application_name ?? "Onyx"}
</p>
</div>

View File

@@ -35,7 +35,7 @@ const DropdownOption: React.FC<DropdownOptionProps> = ({
openInNewTab,
}) => {
const content = (
<div className="flex py-3 px-4 cursor-pointer rounded hover:bg-hover">
<div className="flex py-1.5 text-sm px-2 gap-x-2 text-t text-sm cursor-pointer rounded hover:bg-[#f1eee8]">
{icon}
{label}
</div>
@@ -149,6 +149,7 @@ export function UserDropdown({
inline-block
flex-none
px-2
text-white
text-base
"
>
@@ -163,7 +164,7 @@ export function UserDropdown({
<div
className={`
p-2
min-w-[200px]
w-[175px]
text-strong
text-sm
border
@@ -173,7 +174,6 @@ export function UserDropdown({
shadow-lg
flex
flex-col
w-full
max-h-96
overflow-y-auto
p-1
@@ -189,7 +189,7 @@ export function UserDropdown({
) : hideUserDropdown ? (
<DropdownOption
onClick={() => router.push("/auth/login")}
icon={<UserIcon className="h-5 w-5 my-auto mr-2" />}
icon={<UserIcon className="h-5w-5 my-auto " />}
label="Log In"
/>
) : (
@@ -205,7 +205,6 @@ export function UserDropdown({
h-4
w-4
my-auto
mr-2
overflow-hidden
flex
items-center
@@ -224,7 +223,7 @@ export function UserDropdown({
) : (
<DynamicFaIcon
name={item.icon!}
className="h-4 w-4 my-auto mr-2"
className="h-4 w-4 my-auto "
/>
)
}
@@ -236,18 +235,14 @@ export function UserDropdown({
{showAdminPanel ? (
<DropdownOption
href="/admin/indexing/status"
icon={
<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />
}
icon={<LightSettingsIcon size={16} className="my-auto" />}
label="Admin Panel"
/>
) : (
showCuratorPanel && (
<DropdownOption
href="/admin/indexing/status"
icon={
<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />
}
icon={<LightSettingsIcon size={16} className="my-auto" />}
label="Curator Panel"
/>
)
@@ -256,7 +251,7 @@ export function UserDropdown({
{toggleUserSettings && (
<DropdownOption
onClick={toggleUserSettings}
icon={<UserIcon className="h-5 w-5 my-auto mr-2" />}
icon={<UserIcon size={16} className="my-auto" />}
label="User Settings"
/>
)}
@@ -266,7 +261,7 @@ export function UserDropdown({
setUserInfoVisible(true);
setShowNotifications(true);
}}
icon={<BellIcon className="h-5 w-5 my-auto mr-2" />}
icon={<BellIcon size={16} className="my-auto" />}
label={`Notifications ${
notifications && notifications.length > 0
? `(${notifications.length})`
@@ -284,7 +279,7 @@ export function UserDropdown({
{showLogout && (
<DropdownOption
onClick={handleLogout}
icon={<FiLogOut className="my-auto mr-2 text-lg" />}
icon={<FiLogOut size={16} className="my-auto" />}
label="Log out"
/>
)}

View File

@@ -1,17 +1,27 @@
"use client";
import { ValidSources } from "@/lib/types";
import { SourceIcon } from "./SourceIcon";
import { useState } from "react";
export function WebResultIcon({ url }: { url: string }) {
const [error, setError] = useState(false);
const hostname = new URL(url).hostname;
return hostname == "https://docs.onyx.app" ? (
<img
className="my-0 py-0"
src={`https://www.google.com/s2/favicons?domain=${hostname}`}
alt="favicon"
height={18}
width={18}
/>
) : (
<SourceIcon sourceType={ValidSources.Web} iconSize={18} />
return (
<>
{!error ? (
<img
className="my-0 w-5 h-5 rounded-full py-0"
src={`https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${hostname}&size=128`}
style={{ background: "transparent" }}
alt="favicon"
height={64}
onError={() => setError(true)}
width={64}
/>
) : (
<SourceIcon sourceType={ValidSources.Web} iconSize={18} />
)}
</>
);
}

View File

@@ -9,12 +9,7 @@ export default function CardSection({
className?: string;
}) {
return (
<div
className={cn(
"p-6 shadow-sm rounded-lg bg-white border border-border-strong/80",
className
)}
>
<div className={cn("p-6 border border-border-strong/80", className)}>
{children}
</div>
);

View File

@@ -91,7 +91,7 @@ export function ExplanationText({
}) {
return link ? (
<a
className="underline text-text-500 cursor-pointer text-sm font-medium"
className="underline text-text-500 cursor-pointer text-xs font-medium"
target="_blank"
href={link}
>
@@ -142,6 +142,7 @@ export function TextFormField({
explanationText,
explanationLink,
small,
maxWidth,
removeLabel,
min,
includeForgotPassword,
@@ -165,6 +166,7 @@ export function TextFormField({
defaultHeight?: string;
isCode?: boolean;
fontSize?: "sm" | "md" | "lg";
maxWidth?: string;
hideError?: boolean;
tooltip?: string;
explanationText?: string;
@@ -210,10 +212,10 @@ export function TextFormField({
},
};
const sizeClass = textSizeClasses[fontSize || "md"];
const sizeClass = textSizeClasses[fontSize || "sm"];
return (
<div className={`w-full ${width}`}>
<div className={`w-full ${maxWidth} ${width}`}>
<div
className={`flex ${
vertical ? "flex-col" : "flex-row"
@@ -262,10 +264,11 @@ export function TextFormField({
mt-1
placeholder:font-description
placeholder:${sizeClass.placeholder}
placeholder:text-text-400
caret-accent
placeholder:text-text-muted
${heightString}
${sizeClass.input}
${disabled ? " bg-background-strong" : " bg-white"}
${disabled ? " bg-background-strong" : " bg-white/80"}
${isCode ? " font-mono" : ""}
`}
disabled={disabled}
@@ -625,6 +628,8 @@ interface SelectorFormFieldProps {
defaultValue?: string;
tooltip?: string;
includeReset?: boolean;
fontSize?: "sm" | "md" | "lg";
small?: boolean;
}
export function SelectorFormField({
@@ -638,6 +643,8 @@ export function SelectorFormField({
defaultValue,
tooltip,
includeReset = false,
fontSize = "sm",
small = false,
}: SelectorFormFieldProps) {
const [field] = useField<string>(name);
const { setFieldValue } = useFormikContext();
@@ -647,11 +654,33 @@ export function SelectorFormField({
(option) => option.value?.toString() === field.value?.toString()
);
const textSizeClasses = {
sm: {
label: "text-sm",
input: "text-sm",
placeholder: "text-sm",
},
md: {
label: "text-base",
input: "text-base",
placeholder: "text-base",
},
lg: {
label: "text-lg",
input: "text-lg",
placeholder: "text-lg",
},
};
const sizeClass = textSizeClasses[fontSize];
return (
<div>
{label && (
<div className="flex gap-x-2 items-center">
<Label>{label}</Label>
<Label className={sizeClass.label} small={small}>
{label}
</Label>
{tooltip && <ToolTipDetails>{tooltip}</ToolTipDetails>}
</div>
)}
@@ -668,7 +697,7 @@ export function SelectorFormField({
}
defaultValue={defaultValue}
>
<SelectTrigger>
<SelectTrigger className={sizeClass.input}>
<SelectValue placeholder="Select...">
{currentlySelected?.name || defaultValue || ""}
</SelectValue>
@@ -680,6 +709,7 @@ export function SelectorFormField({
className={`
${maxHeight ? `${maxHeight}` : "max-h-72"}
overflow-y-scroll
${sizeClass.input}
`}
container={container}
>

View File

@@ -1,122 +0,0 @@
import { Persona } from "../../app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
export default function AssistantBanner({
recentAssistants,
liveAssistant,
allAssistants,
onAssistantChange,
mobile = false,
}: {
mobile?: boolean;
recentAssistants: Persona[];
liveAssistant: Persona | undefined;
allAssistants: Persona[];
onAssistantChange: (assistant: Persona) => void;
}) {
return (
<div className="flex mx-auto mt-2 gap-4 ">
{recentAssistants
// First filter out the current assistant
.filter((assistant) => assistant.id !== liveAssistant?.id)
// Combine with visible assistants to get up to 4 total
.concat(
allAssistants.filter(
(assistant) =>
// Exclude current assistant
assistant.id !== liveAssistant?.id &&
// Exclude assistants already in recentAssistants
!recentAssistants.some((recent) => recent.id === assistant.id)
)
)
// Take first 4
.slice(0, mobile ? 2 : 4)
.map((assistant) => (
<TooltipProvider key={assistant.id}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={`${
mobile ? "w-full" : "w-36 mx-3"
} flex py-1.5 scale-[1.] rounded-full border border-border-recent-assistants justify-center items-center gap-x-2 py-1 px-3 hover:bg-background-125 transition-colors cursor-pointer`}
onClick={() => onAssistantChange(assistant)}
>
<AssistantIcon
disableToolip
size="xs"
assistant={assistant}
/>
<span className="font-semibold text-text-recent-assistants text-xs truncate max-w-[120px]">
{assistant.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent backgroundColor="bg-background">
<AssistantCard assistant={assistant} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
);
}
export function AssistantCard({ assistant }: { assistant: Persona }) {
return (
<div className="p-6 backdrop-blur-sm rounded-lg max-w-md w-full mx-auto">
<div className="flex items-center mb-4">
<div className="mb-auto mt-2">
<AssistantIcon disableToolip size="small" assistant={assistant} />
</div>
<div className="ml-3">
<h2 className="text-lg font-semibold text-gray-800">
{assistant.name}
</h2>
<p className="text-sm text-gray-600">{assistant.description}</p>
</div>
</div>
{assistant.tools.length > 0 ||
assistant.llm_relevance_filter ||
assistant.llm_filter_extraction ? (
<div className="space-y-4">
<h3 className="text-base font-medium text-gray-800">Capabilities</h3>
<ul className="space-y-2">
{assistant.tools.map((tool, index) => (
<li
key={index}
className="flex items-center text-sm text-gray-700"
>
<span className="mr-2 text-gray-500"></span>
{tool.display_name}
</li>
))}
{assistant.llm_relevance_filter && (
<li className="flex items-center text-sm text-gray-700">
<span className="mr-2 text-gray-500"></span>
Advanced Relevance Filtering
</li>
)}
{assistant.llm_filter_extraction && (
<li className="flex items-center text-sm text-gray-700">
<span className="mr-2 text-gray-500"></span>
Smart Information Extraction
</li>
)}
</ul>
</div>
) : (
<p className="text-sm text-gray-600 italic">
No specific capabilities listed for this assistant.
</p>
)}
</div>
);
}

View File

@@ -1,63 +0,0 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import React from "react";
export function DisplayAssistantCard({
selectedPersona,
}: {
selectedPersona: Persona;
}) {
return (
<div className="p-4 bg-white/90 backdrop-blur-sm rounded-lg shadow-md border border-border/50 max-w-md w-full mx-auto transition-all duration-300 ease-in-out hover:shadow-lg">
<div className="flex items-center mb-3">
<AssistantIcon
disableToolip
size="medium"
assistant={selectedPersona}
/>
<h2 className="ml-3 text-xl font-semibold text-text-900">
{selectedPersona.name}
</h2>
</div>
<p className="text-sm text-text-600 mb-3 leading-relaxed">
{selectedPersona.description}
</p>
{selectedPersona.tools.length > 0 ||
selectedPersona.llm_relevance_filter ||
selectedPersona.llm_filter_extraction ? (
<div className="space-y-2">
<h3 className="text-base font-medium text-text-900">Capabilities:</h3>
<ul className="space-y-.5">
{/* display all tools */}
{selectedPersona.tools.map((tool, index) => (
<li
key={index}
className="flex items-center text-sm text-text-700"
>
<span className="mr-2 text-text-500 opacity-70"></span>{" "}
{tool.display_name}
</li>
))}
{/* Built in capabilities */}
{selectedPersona.llm_relevance_filter && (
<li className="flex items-center text-sm text-text-700">
<span className="mr-2 text-text-500 opacity-70"></span>{" "}
Advanced Relevance Filtering
</li>
)}
{selectedPersona.llm_filter_extraction && (
<li className="flex items-center text-sm text-text-700">
<span className="mr-2 text-text-500 opacity-70"></span> Smart
Information Extraction
</li>
)}
</ul>
</div>
) : (
<p className="text-sm text-text-600 italic">
No specific capabilities listed for this assistant.
</p>
)}
</div>
);
}

View File

@@ -1,107 +1,142 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import React from "react";
import { createSVG } from "@/lib/assistantIconUtils";
import { buildImgUrl } from "@/app/chat/files/images/utils";
import crypto from "crypto";
import { Persona } from "@/app/admin/assistants/interfaces";
import { CustomTooltip } from "../tooltip/CustomTooltip";
import { buildImgUrl } from "@/app/chat/files/images/utils";
import { OnyxIcon } from "../icons/icons";
export function darkerGenerateColorFromId(id: string): string {
const hash = Array.from(id).reduce(
(acc, char) => acc + char.charCodeAt(0),
0
type IconSize = number | "xs" | "small" | "medium" | "large" | "header";
function md5ToBits(str: string): number[] {
const md5hex = crypto.createHash("md5").update(str).digest("hex");
const bits: number[] = [];
for (let i = 0; i < md5hex.length; i += 2) {
const byteVal = parseInt(md5hex.substring(i, i + 2), 16);
for (let b = 7; b >= 0; b--) {
bits.push((byteVal >> b) & 1);
}
}
return bits;
}
export function generateIdenticon(str: string, dimension: number) {
const bits = md5ToBits(str);
const gridSize = 5;
const halfCols = 4;
const cellSize = dimension / gridSize;
let bitIndex = 0;
const squares: JSX.Element[] = [];
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < halfCols; col++) {
const bit = bits[bitIndex % bits.length];
bitIndex++;
if (bit === 1) {
const xPos = col * cellSize;
const yPos = row * cellSize;
squares.push(
<rect
key={`${xPos}-${yPos}`}
x={xPos - 0.5}
y={yPos - 0.5}
width={cellSize}
height={cellSize}
fill="black"
/>
);
const mirrorCol = gridSize - 1 - col;
if (mirrorCol !== col) {
const mirrorX = mirrorCol * cellSize;
squares.push(
<rect
key={`a-${mirrorX}-${yPos}`}
x={mirrorX - 0.5}
y={yPos - 0.5}
width={cellSize}
height={cellSize}
fill="black"
/>
);
}
}
}
}
return (
<svg
width={dimension}
height={dimension}
viewBox={`0 0 ${dimension} ${dimension}`}
style={{ display: "block" }}
>
{squares}
</svg>
);
const hue = (hash * 137.508) % 360; // Use golden angle approximation
// const saturation = 40 + (hash % 10); // Saturation between 40-50%
// const lightness = 40 + (hash % 10); // Lightness between 40-50%
const saturation = 35 + (hash % 10); // Saturation between 40-50%
const lightness = 35 + (hash % 10); // Lightness between 40-50%
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
export function AssistantIcon({
assistant,
size,
border,
className,
disableToolip,
}: {
assistant: Persona;
size?: "xs" | "small" | "medium" | "large" | "header";
size?: IconSize;
className?: string;
border?: boolean;
disableToolip?: boolean;
}) {
const color = darkerGenerateColorFromId(assistant.id.toString());
const dimension =
typeof size === "number"
? size
: (() => {
switch (size) {
case "xs":
return 16;
case "small":
return 24;
case "medium":
return 32;
case "large":
return 40;
case "header":
return 56;
default:
return 24;
}
})();
const wrapperClass = border ? "ring ring-[1px] ring-border-strong" : "";
const style = { width: dimension, height: dimension };
return (
<CustomTooltip
className={className}
disabled={disableToolip || !assistant.description}
showTick
line
wrap
content={assistant.description}
>
{
// Prioritization order: image, graph, defaults
assistant.uploaded_image_id ? (
<img
alt={assistant.name}
className={`object-cover object-center rounded-sm overflow-hidden transition-opacity duration-300 opacity-100
${
size === "large"
? "w-10 h-10"
: size === "header"
? "w-14 h-14"
: size === "medium"
? "w-8 h-8"
: size === "xs"
? "w-4 h-4"
: "w-6 h-6"
}`}
src={buildImgUrl(assistant.uploaded_image_id)}
loading="lazy"
/>
) : assistant.icon_shape && assistant.icon_color ? (
<div
className={`flex-none
${border && "ring ring-[1px] ring-border-strong "}
${
size === "large"
? "w-10 h-10"
: size === "header"
? "w-14 h-14"
: size === "medium"
? "w-8 h-8"
: size === "xs"
? "w-4 h-4"
: "w-6 h-6"
} `}
>
{createSVG(
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
assistant.icon_color,
size === "large"
? 40
: size === "header"
? 56
: size === "medium"
? 32
: size === "xs"
? 16
: 24
)}
</div>
) : (
<div
className={`flex-none rounded-sm overflow-hidden
${border && "border border-.5 border-border-strong "}
${size === "large" ? "w-10 h-10" : ""}
${size === "header" ? "w-14 h-14" : ""}
${size === "medium" ? "w-8 h-8" : ""}
${size === "xs" ? "w-4 h-4" : ""}
${!size || size === "small" ? "w-6 h-6" : ""} `}
style={{ backgroundColor: color }}
/>
)
}
{assistant.id == 0 ? (
<OnyxIcon size={dimension} />
) : assistant.uploaded_image_id ? (
<img
alt={assistant.name}
src={buildImgUrl(assistant.uploaded_image_id)}
loading="lazy"
className={`object-cover object-center rounded-sm transition-opacity duration-300 ${wrapperClass}`}
style={style}
/>
) : (
<div className={wrapperClass} style={style}>
{generateIdenticon((assistant.icon_shape || 0).toString(), dimension)}
</div>
)}
</CustomTooltip>
);
}

View File

@@ -17,6 +17,7 @@ export function StarterMessages({
className={`
mx-auto
w-full
${
isMobile
? "gap-x-2 w-2/3 justify-between"
@@ -37,7 +38,8 @@ export function StarterMessages({
onClick={() => onSubmit(starterMessage.message)}
className={`relative flex ${
!isMobile && "w-40"
} flex-col gap-2 rounded-2xl shadow-sm border border-border bg-background-starter-message px-3 py-2 text-start align-to text-wrap text-[15px] shadow-xs transition enabled:hover:bg-background-starter-message-hover disabled:cursor-not-allowed line-clamp-3`}
} flex-col gap-2 rounded-md shadow-sm text-text-dark hover:text-text border border-border bg-background-starter-message px-3 py-2 text-start align-to text-wrap text-[15px] shadow-xs transition
enabled:hover:bg-background-dark/75 disabled:cursor-not-allowed line-clamp-3`}
style={{ height: `5.2rem` }}
>
{starterMessage.name}

View File

@@ -26,7 +26,7 @@ export default function AuthFlowContainer({
</div>
)}
{authState === "signup" && (
<div className="text-sm mt-4 text-center w-full text-neutral-900 font-medium mx-auto">
<div className="text-sm mt-4 text-center w-full text-neutral-800 font-medium mx-auto">
Already have an account?{" "}
<Link
href="/auth/login"

View File

@@ -66,10 +66,12 @@ const AssistantSelector = ({
const [isTemperatureExpanded, setIsTemperatureExpanded] = useState(false);
// Initialize selectedTab from localStorage
const [selectedTab, setSelectedTab] = useState<number>(() => {
const [selectedTab, setSelectedTab] = useState<number | undefined>();
useEffect(() => {
const storedTab = localStorage.getItem("assistantSelectorSelectedTab");
return storedTab !== null ? Number(storedTab) : 0;
});
const tab = storedTab !== null ? Number(storedTab) : 0;
setSelectedTab(tab);
}, [localStorage]);
const sensors = useSensors(
useSensor(PointerSensor, {

View File

@@ -19,14 +19,12 @@ export default function FunctionalHeader({
toggleSidebar = () => null,
reset = () => null,
sidebarToggled,
documentSidebarToggled,
toggleUserSettings,
hideUserDropdown,
}: {
reset?: () => void;
page: pageType;
sidebarToggled?: boolean;
documentSidebarToggled?: boolean;
currentChatSession?: ChatSession | null | undefined;
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
toggleSidebar?: () => void;
@@ -135,7 +133,7 @@ export default function FunctionalHeader({
}
>
<div className="cursor-pointer ml-2 mr-4 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
<NewChatIcon size={20} />
<NewChatIcon size={40} />
</div>
</Link>
<div
@@ -149,7 +147,6 @@ export default function FunctionalHeader({
duration-300
ease-in-out
h-full
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
`}
/>
</div>
@@ -160,11 +157,7 @@ export default function FunctionalHeader({
h-20 absolute top-0 z-10 w-full sm:w-[90%] lg:w-[70%]
bg-gradient-to-b via-50% z-[-1] from-background via-background to-background/10 flex
transition-all duration-300 ease-in-out
${
documentSidebarToggled
? "left-[200px] transform -translate-x-[calc(50%+100px)]"
: "left-1/2 transform -translate-x-1/2"
}
left-1/2 transform -translate-x-1/2
`}
/>
)}

View File

@@ -72,15 +72,20 @@ export const useSidebarVisibility = ({
};
const handleMouseLeave = () => {
setShowDocSidebar(false);
if (!mobile) {
setShowDocSidebar(false);
}
};
document.addEventListener("mousemove", handleEvent);
document.addEventListener("mouseleave", handleMouseLeave);
if (!mobile) {
document.addEventListener("mousemove", handleEvent);
document.addEventListener("mouseleave", handleMouseLeave);
}
return () => {
document.removeEventListener("mousemove", handleEvent);
document.removeEventListener("mouseleave", handleMouseLeave);
if (!mobile) {
document.removeEventListener("mousemove", handleEvent);
document.removeEventListener("mouseleave", handleMouseLeave);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showDocSidebar, toggledSidebar, sidebarElementRef, mobile]);

View File

@@ -15,7 +15,7 @@ export default function SourceCard({
<div
key={doc.document_id}
onClick={() => openDocument(doc, setPresentingDocument)}
className="cursor-pointer text-left overflow-hidden flex flex-col gap-0.5 rounded-sm px-3 py-2.5 hover:bg-background-125 bg-background-100 w-[200px]"
className="cursor-pointer text-left overflow-hidden flex flex-col gap-0.5 rounded-lg px-3 py-2 hover:bg-background-dark/80 bg-background-dark/60 w-[200px]"
>
<div className="line-clamp-1 font-semibold text-ellipsis text-text-900 flex h-6 items-center gap-2 text-sm">
{doc.is_internet || doc.source_type === "web" ? (
@@ -23,7 +23,7 @@ export default function SourceCard({
) : (
<SourceIcon sourceType={doc.source_type} iconSize={18} />
)}
<p>{truncateString(doc.semantic_identifier || doc.document_id, 12)}</p>
<p>{truncateString(doc.semantic_identifier || doc.document_id, 20)}</p>
</div>
<div className="line-clamp-2 text-sm font-semibold"></div>
<div className="line-clamp-2 text-sm font-normal leading-snug text-text-700">
@@ -48,17 +48,16 @@ export function SeeMoreBlock({
<div
onClick={toggleDocumentSelection}
className={`
${documentSelectionToggled ? "border-border-100 border" : ""}
cursor-pointer rounded-sm flex-none transition-all duration-500 hover:bg-background-125 bg-text-100 px-3 py-2.5
cursor-pointer rounded-lg flex-none transition-all duration-500 hover:bg-background-dark/80 bg-background-dark/60 px-3 py-2
`}
>
<div className="flex h-6 items-center text-sm">
<div className="flex gap-y-2 flex-col items-start text-sm">
<p className="flex-1 mr-1 font-semibold text-text-900 overflow-hidden text-ellipsis whitespace-nowrap">
{documentSelectionToggled ? "Hide sources" : "See context"}
Full Results
</p>
<div className="flex-shrink-0 flex items-center">
<div className="flex-shrink-0 flex gap-x-1 items-center">
{uniqueSources.slice(0, 3).map((sourceType, ind) => (
<div key={ind} className="inline-block ml-1">
<div key={ind} className="inline-block ">
<SourceIcon sourceType={sourceType} iconSize={16} />
</div>
))}
@@ -69,9 +68,6 @@ export function SeeMoreBlock({
)}
</div>
</div>
<div className="line-clamp-2 text-sm font-normal leading-snug text-text-700 mt-1">
See more
</div>
</div>
);
}

View File

@@ -21,6 +21,7 @@ interface AssistantsContextProps {
hiddenAssistants: Persona[];
finalAssistants: Persona[];
ownedButHiddenAssistants: Persona[];
pinnedAssistants: Persona[];
refreshAssistants: () => Promise<void>;
isImageGenerationAvailable: boolean;
recentAssistants: Persona[];
@@ -172,12 +173,22 @@ export const AssistantsProvider: React.FC<{
visibleAssistants,
hiddenAssistants,
finalAssistants,
pinnedAssistants,
ownedButHiddenAssistants,
} = useMemo(() => {
const { visibleAssistants, hiddenAssistants } = classifyAssistants(
user,
assistants
);
const pinnedAssistants = user?.preferences.pinned_assistants
? visibleAssistants.filter((assistant) =>
user.preferences.pinned_assistants.includes(assistant.id)
)
: visibleAssistants.slice(0, 3);
// Fallback to first 3 assistants if pinnedAssistants is empty
const finalPinnedAssistants =
pinnedAssistants.length > 0 ? pinnedAssistants : assistants.slice(0, 3);
const finalAssistants = user
? orderAssistantsForUser(visibleAssistants, user)
@@ -192,6 +203,7 @@ export const AssistantsProvider: React.FC<{
visibleAssistants,
hiddenAssistants,
finalAssistants,
pinnedAssistants,
ownedButHiddenAssistants,
};
}, [user, assistants]);
@@ -203,6 +215,7 @@ export const AssistantsProvider: React.FC<{
visibleAssistants,
hiddenAssistants,
finalAssistants,
pinnedAssistants,
ownedButHiddenAssistants,
refreshAssistants,
editablePersonas,

View File

@@ -0,0 +1,123 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
import { Shortcut } from "@/app/chat/nrf/interfaces";
import { notifyExtensionOfThemeChange } from "@/lib/extension/utils";
import {
darkExtensionImages,
lightExtensionImages,
LocalStorageKeys,
} from "@/lib/extension/constants";
interface NRFPreferencesContextValue {
theme: string;
setTheme: (t: string) => void;
defaultLightBackgroundUrl: string;
setDefaultLightBackgroundUrl: (val: string) => void;
defaultDarkBackgroundUrl: string;
setDefaultDarkBackgroundUrl: (val: string) => void;
shortcuts: Shortcut[];
setShortcuts: (s: Shortcut[]) => void;
useOnyxAsNewTab: boolean;
setUseOnyxAsNewTab: (v: boolean) => void;
showShortcuts: boolean;
setShowShortcuts: (v: boolean) => void;
}
const NRFPreferencesContext = createContext<
NRFPreferencesContextValue | undefined
>(undefined);
function useLocalStorageState<T>(
key: string,
defaultValue: T
): [T, (value: T) => void] {
const [state, setState] = useState<T>(() => {
if (typeof window !== "undefined") {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : defaultValue;
}
return undefined;
});
const setValue = (value: T) => {
setState(value);
if (typeof window !== "undefined") {
localStorage.setItem(key, JSON.stringify(value));
}
};
return [state, setValue];
}
export function NRFPreferencesProvider({
children,
}: {
children: React.ReactNode;
}) {
const [theme, setTheme] = useLocalStorageState<string>(
LocalStorageKeys.THEME,
"dark"
);
const [defaultLightBackgroundUrl, setDefaultLightBackgroundUrl] =
useLocalStorageState<string>(
LocalStorageKeys.LIGHT_BG_URL,
lightExtensionImages[0]
);
const [defaultDarkBackgroundUrl, setDefaultDarkBackgroundUrl] =
useLocalStorageState<string>(
LocalStorageKeys.DARK_BG_URL,
darkExtensionImages[0]
);
const [shortcuts, setShortcuts] = useLocalStorageState<Shortcut[]>(
LocalStorageKeys.SHORTCUTS,
[]
);
const [showShortcuts, setShowShortcuts] = useLocalStorageState<boolean>(
LocalStorageKeys.SHOW_SHORTCUTS,
false
);
const [useOnyxAsNewTab, setUseOnyxAsNewTab] = useLocalStorageState<boolean>(
LocalStorageKeys.USE_ONYX_AS_NEW_TAB,
true
);
useEffect(() => {
if (theme === "dark") {
notifyExtensionOfThemeChange(theme, defaultDarkBackgroundUrl);
} else {
notifyExtensionOfThemeChange(theme, defaultLightBackgroundUrl);
}
}, [theme, defaultLightBackgroundUrl, defaultDarkBackgroundUrl]);
return (
<NRFPreferencesContext.Provider
value={{
theme,
setTheme,
defaultLightBackgroundUrl,
setDefaultLightBackgroundUrl,
defaultDarkBackgroundUrl,
setDefaultDarkBackgroundUrl,
shortcuts,
setShortcuts,
useOnyxAsNewTab,
setUseOnyxAsNewTab,
showShortcuts,
setShowShortcuts,
}}
>
{children}
</NRFPreferencesContext.Provider>
);
}
export function useNRFPreferences() {
const context = useContext(NRFPreferencesContext);
if (!context) {
throw new Error(
"useNRFPreferences must be used within an NRFPreferencesProvider"
);
}
return context;
}

View File

@@ -0,0 +1,257 @@
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Shortcut } from "@/app/chat/nrf/interfaces";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PencilIcon, PlusIcon } from "lucide-react";
import Image from "next/image";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { Modal } from "../Modal";
import { QuestionMarkIcon } from "../icons/icons";
export const validateUrl = (input: string) => {
try {
new URL(input);
return true;
} catch {
return false;
}
};
export const ShortCut = ({
shortCut,
onEdit,
}: {
shortCut: Shortcut;
onEdit: (shortcut: Shortcut) => void;
}) => {
const [faviconError, setFaviconError] = useState(false);
return (
<div className="w-24 h-24 flex-none rounded-xl shadow-lg relative group transition-all duration-300 ease-in-out hover:scale-105 bg-white/10 backdrop-blur-sm">
<button
onClick={(e) => {
e.stopPropagation();
onEdit(shortCut);
}}
className="absolute top-1 right-1 p-1 bg-white/20 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200"
>
<PencilIcon className="w-3 h-3 text-white" />
</button>
<div
onClick={() => window.open(shortCut.url, "_blank")}
className="w-full h-full flex flex-col items-center justify-center cursor-pointer"
>
<div className="w-8 h-8 mb-2 relative">
{shortCut.favicon && !faviconError ? (
<Image
src={shortCut.favicon}
alt={shortCut.name}
width={40}
height={40}
className="rounded-sm"
onError={() => setFaviconError(true)}
/>
) : (
<QuestionMarkIcon size={32} className="text-white w-full h-full" />
)}
</div>
<h1 className="text-white w-full text-center font-semibold text-sm truncate px-2">
{shortCut.name}
</h1>
</div>
</div>
);
};
export const AddShortCut = ({
openShortCutModal,
}: {
openShortCutModal: () => void;
}) => {
return (
<button
onClick={openShortCutModal}
className="w-24 h-24 flex-none rounded-xl bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all duration-300 ease-in-out flex flex-col items-center justify-center"
>
<PlusIcon className="w-8 h-8 text-white mb-2" />
<h1 className="text-white text-xs font-medium">New Bookmark</h1>
</button>
);
};
export const NewShortCutModal = ({
isOpen,
onClose,
onAdd,
editingShortcut,
onDelete,
setPopup,
}: {
isOpen: boolean;
onClose: () => void;
onDelete: (shortcut: Shortcut) => void;
onAdd: (shortcut: Shortcut) => void;
editingShortcut?: Shortcut | null;
setPopup: (popup: PopupSpec) => void;
}) => {
const [name, setName] = useState(editingShortcut?.name || "");
const [url, setUrl] = useState(editingShortcut?.url || "");
const [faviconError, setFaviconError] = useState(false);
const [isValidUrl, setIsValidUrl] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isValidUrl) {
const faviconUrl = `https://www.google.com/s2/favicons?domain=${
new URL(url).hostname
}&sz=64`;
onAdd({ name, url, favicon: faviconUrl });
onClose();
} else {
console.error("Invalid URL submitted");
setPopup({
type: "error",
message: "Please enter a valid URL",
});
}
};
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newUrl = e.target.value;
setUrl(newUrl);
setIsValidUrl(validateUrl(newUrl));
setFaviconError(false);
};
useEffect(() => {
setIsValidUrl(validateUrl(url));
}, [url]);
const faviconUrl = isValidUrl
? `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=64`
: "";
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[95%] sm:max-w-[425px] bg-neutral-900 border-none text-white">
<DialogHeader>
<DialogTitle>
{editingShortcut ? "Edit Shortcut" : "Add New Shortcut"}
</DialogTitle>
<DialogDescription>
{editingShortcut
? "Modify your existing shortcut."
: "Create a new shortcut for quick access to your favorite websites."}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="w-full space-y-6">
<div className="space-y-4 w-full">
<div className="flex flex-col space-y-2">
<Label
htmlFor="name"
className="text-sm font-medium text-neutral-300"
>
Name
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-neutral-800 border-neutral-700 text-white"
placeholder="Enter shortcut name"
/>
</div>
<div className="flex flex-col space-y-2">
<Label
htmlFor="url"
className="text-sm font-medium text-neutral-300"
>
URL
</Label>
<Input
id="url"
value={url}
onChange={handleUrlChange}
className={`bg-neutral-800 border-neutral-700 text-white ${
!isValidUrl && url ? "border-red-500" : ""
}`}
placeholder="https://example.com"
/>
{!isValidUrl && url && (
<p className="text-red-500 text-sm">Please enter a valid URL</p>
)}
</div>
<div className="flex items-center space-x-2">
<Label className="text-sm font-medium text-neutral-300">
Favicon Preview:
</Label>
<div className="w-8 h-8 relative flex items-center justify-center">
{isValidUrl && !faviconError ? (
<Image
src={faviconUrl}
alt="Favicon"
width={32}
height={32}
className="w-full h-full rounded-sm"
onError={() => setFaviconError(true)}
/>
) : (
<QuestionMarkIcon size={32} className="w-full h-full" />
)}
</div>
</div>
</div>
<DialogFooter>
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 text-white"
disabled={!isValidUrl || !name}
>
{editingShortcut ? "Save Changes" : "Add Shortcut"}
</Button>
{editingShortcut && (
<Button
type="button"
variant="destructive"
onClick={() => onDelete(editingShortcut)}
>
Delete
</Button>
)}
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export const MaxShortcutsReachedModal = ({
onClose,
}: {
onClose: () => void;
}) => {
return (
<Modal
width="max-w-md"
title="Maximum Shortcuts Reached"
onOutsideClick={onClose}
>
<div className="flex flex-col gap-4">
<p className="text-left text-neutral-900">
You&apos;ve reached the maximum limit of 8 shortcuts. To add a new
shortcut, please remove an existing one.
</p>
<Button onClick={onClose}>Close</Button>
</div>
</Modal>
);
};

View File

@@ -105,7 +105,7 @@ export default function LogoWithText({
>
<NewChatIcon
className="ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300"
size={20}
size={24}
/>
</Link>
</TooltipTrigger>
@@ -119,7 +119,7 @@ export default function LogoWithText({
<Tooltip>
<TooltipTrigger asChild>
<button
className="mr-3 my-auto ml-auto"
className="mr-3 my-auto ml-auto"
onClick={() => {
toggleSidebar();
if (toggled) {
@@ -138,7 +138,7 @@ export default function LogoWithText({
/>
</button>
</TooltipTrigger>
<TooltipContent>
<TooltipContent className="!border-none">
{toggled ? `Unpin sidebar` : "Pin sidebar"}
</TooltipContent>
</Tooltip>

View File

@@ -505,6 +505,30 @@ export const FileIcon = ({
return <FiFile size={size} className={className} />;
};
export const FileIcon2 = ({
size = 16,
className = defaultTailwindCSSBlue,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 14 14"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M12.5 12.5a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1h5l5 5Zm-8-8h2m-2 3h5m-5 3h5"
/>
</svg>
);
};
export const InfoIcon = ({
size = 16,
className = defaultTailwindCSS,
@@ -985,9 +1009,9 @@ export const BellIcon = ({
>
<path
fill="currentColor"
fill-rule="evenodd"
fillRule="evenodd"
d="M12 1.25A7.75 7.75 0 0 0 4.25 9v.704a3.53 3.53 0 0 1-.593 1.958L2.51 13.385c-1.334 2-.316 4.718 2.003 5.35c.755.206 1.517.38 2.284.523l.002.005C7.567 21.315 9.622 22.75 12 22.75s4.433-1.435 5.202-3.487l.002-.005a28.472 28.472 0 0 0 2.284-.523c2.319-.632 3.337-3.35 2.003-5.35l-1.148-1.723a3.53 3.53 0 0 1-.593-1.958V9A7.75 7.75 0 0 0 12 1.25Zm3.376 18.287a28.46 28.46 0 0 1-6.753 0c.711 1.021 1.948 1.713 3.377 1.713c1.429 0 2.665-.692 3.376-1.713ZM5.75 9a6.25 6.25 0 1 1 12.5 0v.704c0 .993.294 1.964.845 2.79l1.148 1.723a2.02 2.02 0 0 1-1.15 3.071a26.96 26.96 0 0 1-14.187 0a2.021 2.021 0 0 1-1.15-3.07l1.15-1.724a5.03 5.03 0 0 0 .844-2.79V9Z"
clip-rule="evenodd"
clipRule="evenodd"
/>
</svg>
);
@@ -1061,6 +1085,28 @@ export const GlobeIcon = ({
return <FiGlobe size={size} className={className} />;
};
export const GlobeIcon2 = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
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>
);
};
export const GmailIcon = ({
size = 16,
className = defaultTailwindCSS,
@@ -2322,30 +2368,6 @@ export const PaintingIconSkeleton = ({
);
};
export const NewChatIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 24 24"
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path
strokeLinecap="round"
d="M22 10.5V12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22s-7.071 0-8.536-1.465C2 19.072 2 16.714 2 12s0-7.071 1.464-8.536C4.93 2 7.286 2 12 2h1.5"
/>
<path d="m16.652 3.455l.649-.649A2.753 2.753 0 0 1 21.194 6.7l-.65.649m-3.892-3.893s.081 1.379 1.298 2.595c1.216 1.217 2.595 1.298 2.595 1.298m-3.893-3.893L10.687 9.42c-.404.404-.606.606-.78.829c-.205.262-.38.547-.524.848c-.121.255-.211.526-.392 1.068L8.412 13.9m12.133-6.552l-5.965 5.965c-.404.404-.606.606-.829.78a4.59 4.59 0 0 1-.848.524c-.255.121-.526.211-1.068.392l-1.735.579m0 0l-1.123.374a.742.742 0 0 1-.939-.94l.374-1.122m1.688 1.688L8.412 13.9" />
</g>
</svg>
);
};
export const ImageIcon = ({
size = 16,
className = defaultTailwindCSS,
@@ -2668,7 +2690,7 @@ export const OpenIcon = ({
fill="none"
stroke="currentColor"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
d="M7 13.5a9.26 9.26 0 0 0-5.61-2.95a1 1 0 0 1-.89-1V1.5A1 1 0 0 1 1.64.51A9.3 9.3 0 0 1 7 3.43zm0 0a9.26 9.26 0 0 1 5.61-2.95a1 1 0 0 0 .89-1V1.5a1 1 0 0 0-1.14-.99A9.3 9.3 0 0 0 7 3.43z"
/>
</svg>
@@ -2692,7 +2714,7 @@ export const DexpandTwoIcon = ({
fill="none"
stroke="currentColor"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
d="m.5 13.5l5-5m-4 0h4v4m8-12l-5 5m4 0h-4v-4"
/>
</svg>
@@ -2716,7 +2738,7 @@ export const ExpandTwoIcon = ({
fill="none"
stroke="currentColor"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
d="m8.5 5.5l5-5m-4 0h4v4m-8 4l-5 5m4 0h-4v-4"
/>
</svg>
@@ -2740,7 +2762,7 @@ export const DownloadCSVIcon = ({
fill="none"
stroke="currentColor"
strokeLinecap="round"
stroke-linejoin="round"
strokeLinejoin="round"
d="M.5 10.5v1a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1M4 6l3 3.5L10 6M7 9.5v-9"
/>
</svg>
@@ -2764,8 +2786,8 @@ export const UserIcon = ({
fill="none"
stroke="currentColor"
strokeLinecap="round"
stroke-linejoin="round"
stroke-width="1.5"
strokeLinejoin="round"
strokeWidth="1.5"
d="M19.618 21.25c0-3.602-4.016-6.53-7.618-6.53c-3.602 0-7.618 2.928-7.618 6.53M12 11.456a4.353 4.353 0 1 0 0-8.706a4.353 4.353 0 0 0 0 8.706"
/>
</svg>
@@ -2799,3 +2821,176 @@ export const AirtableIcon = ({
</div>
);
};
export const PinnedIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 14"
fill="none"
>
<path
d="M5.33165 8.74445L1 13M2.33282 5.46113L8.4591 11.4798L9.58999 10.3688L9.32809 7.88941L13 4.83L9.10152 1L5.98673 4.6074L3.46371 4.3501L2.33282 5.46113Z"
stroke="black"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export const OnyxIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
viewBox="0 0 56 56"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="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>
);
};
export const QuestionMarkIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
);
};
export const NewChatIcon = ({
size = 24,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.5 1.99982H6C3.79086 1.99982 2 3.79068 2 5.99982V13.9998C2 16.209 3.79086 17.9998 6 17.9998H14C16.2091 17.9998 18 16.209 18 13.9998V8.49982"
stroke="black"
strokeLinecap="round"
/>
<path
d="M17.1471 5.13076C17.4492 4.82871 17.6189 4.41901 17.619 3.9918C17.6191 3.56458 17.4494 3.15484 17.1474 2.85271C16.8453 2.55058 16.4356 2.38082 16.0084 2.38077C15.5812 2.38071 15.1715 2.55037 14.8693 2.85242L11.0562 6.66651L7.24297 10.4806C7.1103 10.6129 7.01218 10.7758 6.95726 10.9549L6.20239 13.4418C6.18762 13.4912 6.18651 13.5437 6.19916 13.5937C6.21182 13.6437 6.23778 13.6894 6.27428 13.7258C6.31078 13.7623 6.35646 13.7881 6.40648 13.8007C6.45651 13.8133 6.509 13.8121 6.5584 13.7972L9.04585 13.0429C9.2248 12.9885 9.38766 12.891 9.52014 12.7589L17.1471 5.13076Z"
stroke="black"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export const Caret = ({
size = 24,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m12.37 15.835l6.43-6.63C19.201 8.79 18.958 8 18.43 8H5.57c-.528 0-.771.79-.37 1.205l6.43 6.63c.213.22.527.22.74 0Z"
/>
</svg>
);
};
export const OpenAISVG = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
>
<path
fill="currentColor"
d="M45.403,25.562c-0.506-1.89-1.518-3.553-2.906-4.862c1.134-2.665,0.963-5.724-0.487-8.237 c-1.391-2.408-3.636-4.131-6.322-4.851c-1.891-0.506-3.839-0.462-5.669,0.088C28.276,5.382,25.562,4,22.647,4 c-4.906,0-9.021,3.416-10.116,7.991c-0.01,0.001-0.019-0.003-0.029-0.002c-2.902,0.36-5.404,2.019-6.865,4.549 c-1.391,2.408-1.76,5.214-1.04,7.9c0.507,1.891,1.519,3.556,2.909,4.865c-1.134,2.666-0.97,5.714,0.484,8.234 c1.391,2.408,3.636,4.131,6.322,4.851c0.896,0.24,1.807,0.359,2.711,0.359c1.003,0,1.995-0.161,2.957-0.45 C21.722,44.619,24.425,46,27.353,46c4.911,0,9.028-3.422,10.12-8.003c2.88-0.35,5.431-2.006,6.891-4.535 C45.754,31.054,46.123,28.248,45.403,25.562z M35.17,9.543c2.171,0.581,3.984,1.974,5.107,3.919c1.049,1.817,1.243,4,0.569,5.967 c-0.099-0.062-0.193-0.131-0.294-0.19l-9.169-5.294c-0.312-0.179-0.698-0.177-1.01,0.006l-10.198,6.041l-0.052-4.607l8.663-5.001 C30.733,9.26,33,8.963,35.17,9.543z M29.737,22.195l0.062,5.504l-4.736,2.805l-4.799-2.699l-0.062-5.504l4.736-2.805L29.737,22.195z M14.235,14.412C14.235,9.773,18.009,6,22.647,6c2.109,0,4.092,0.916,5.458,2.488C28,8.544,27.891,8.591,27.787,8.651l-9.17,5.294 c-0.312,0.181-0.504,0.517-0.5,0.877l0.133,11.851l-4.015-2.258V14.412z M6.528,23.921c-0.581-2.17-0.282-4.438,0.841-6.383 c1.06-1.836,2.823-3.074,4.884-3.474c-0.004,0.116-0.018,0.23-0.018,0.348V25c0,0.361,0.195,0.694,0.51,0.872l10.329,5.81 L19.11,34.03l-8.662-5.002C8.502,27.905,7.11,26.092,6.528,23.921z M14.83,40.457c-2.171-0.581-3.984-1.974-5.107-3.919 c-1.053-1.824-1.249-4.001-0.573-5.97c0.101,0.063,0.196,0.133,0.299,0.193l9.169,5.294c0.154,0.089,0.327,0.134,0.5,0.134 c0.177,0,0.353-0.047,0.51-0.14l10.198-6.041l0.052,4.607l-8.663,5.001C19.269,40.741,17.001,41.04,14.83,40.457z M35.765,35.588 c0,4.639-3.773,8.412-8.412,8.412c-2.119,0-4.094-0.919-5.459-2.494c0.105-0.056,0.216-0.098,0.32-0.158l9.17-5.294 c0.312-0.181,0.504-0.517,0.5-0.877L31.75,23.327l4.015,2.258V35.588z M42.631,32.462c-1.056,1.83-2.84,3.086-4.884,3.483 c0.004-0.12,0.018-0.237,0.018-0.357V25c0-0.361-0.195-0.694-0.51-0.872l-10.329-5.81l3.964-2.348l8.662,5.002 c1.946,1.123,3.338,2.937,3.92,5.107C44.053,28.249,43.754,30.517,42.631,32.462z"
/>
</svg>
);
};
export const AnthropicSVG = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 92.2 65"
xmlSpace="preserve"
fill="currentColor"
>
<path
fill="currentColor"
d="M66.5,0H52.4l25.7,65h14.1L66.5,0z M25.7,0L0,65h14.4l5.3-13.6h26.9L51.8,65h14.4L40.5,0C40.5,0,25.7,0,25.7,0z M24.3,39.3l8.8-22.8l8.8,22.8H24.3z"
/>
</svg>
);
};
export const SourcesIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
viewBox="0 0 28 29"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6 22.5L14 14.5L22 6.5V14.5H14V22.5H6Z" fill="black" />
</svg>
);
};

View File

@@ -1,4 +1,3 @@
import { cookies } from "next/headers";
import {
_CompletedWelcomeFlowDummyComponent,
_WelcomeModal,

View File

@@ -10,7 +10,7 @@ export const ApiKeyModal = ({
hide,
setPopup,
}: {
hide: () => void;
hide?: () => void;
setPopup: (popup: PopupSpec) => void;
}) => {
const router = useRouter();
@@ -28,18 +28,25 @@ export const ApiKeyModal = ({
<Modal
title="Configure a Generative AI Model"
width="max-w-3xl w-full"
onOutsideClick={() => hide()}
onOutsideClick={hide ? () => hide() : undefined}
>
<>
<div className="mb-5 text-sm text-gray-700">
Please provide an API Key you can always change this or switch
models later.
<br />
If you would rather look around first, you can{" "}
<strong onClick={() => hide()} className="text-link cursor-pointer">
skip this step
</strong>
.
{hide && (
<>
If you would rather look around first, you can{" "}
<strong
onClick={() => hide()}
className="text-link cursor-pointer"
>
skip this step
</strong>
.
</>
)}
</div>
<ApiKeyForm
@@ -47,7 +54,7 @@ export const ApiKeyModal = ({
onSuccess={() => {
router.refresh();
refreshProviderInfo();
hide();
hide?.();
}}
providerOptions={providerOptions}
/>

View File

@@ -73,7 +73,7 @@ export const LlmList: React.FC<LlmListProps> = ({
scrollable
? "max-h-[200px] default-scrollbar overflow-x-hidden"
: "max-h-[300px]"
} bg-background-175 flex flex-col gap-y-1 overflow-y-scroll`}
} bg-background-175 flex flex-col gap-y-2 mt-1 overflow-y-scroll`}
>
{llmOptions.map(({ name, icon, value }, index) => {
if (!requiresImageGeneration || checkLLMSupportsImageInput(name)) {
@@ -81,15 +81,33 @@ export const LlmList: React.FC<LlmListProps> = ({
<button
type="button"
key={index}
className={`w-full py-1.5 flex gap-x-2 px-2 text-sm ${
currentLlm == name
? "bg-background-200"
: "bg-background hover:bg-background-100"
} text-left rounded`}
className={`w-full items-center flex gap-x-2 text-sm text-left rounded`}
onClick={() => onSelect(value)}
>
<div className="relative flex-shrink-0">
<svg
width="12"
height="12"
viewBox="0 0 20 20"
fill="none"
className={`overflow-hidden rounded-full ${
currentLlm == name ? "bg-accent border-none " : ""
}`}
xmlns="http://www.w3.org/2000/svg"
>
{currentLlm != name && (
<circle
cx="10"
cy="10"
r="9"
stroke="currentColor"
strokeWidth="1.5"
/>
)}
</svg>
</div>
{icon({ size: 16 })}
{getDisplayNameForModel(name)}
<p className="text-sm">{getDisplayNameForModel(name)}</p>
{(() => {
if (
currentAssistant?.llm_model_version_override === name &&

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