forked from github/onyx
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18794aeca0 | ||
|
|
919296b717 | ||
|
|
5b9d2ee322 | ||
|
|
f34c8bdc71 | ||
|
|
de988149a3 | ||
|
|
309eeeb90c | ||
|
|
8e9bfaa280 | ||
|
|
d88d74d48b | ||
|
|
7dd6eb9cb0 | ||
|
|
a8f90a824f | ||
|
|
2e163eb1c9 | ||
|
|
195fa58b49 | ||
|
|
20f48894d5 | ||
|
|
bf5312862a | ||
|
|
c1c1da2736 | ||
|
|
a301d0e728 | ||
|
|
c0b303b445 | ||
|
|
4042493db4 | ||
|
|
5858a682c0 | ||
|
|
cd79614343 | ||
|
|
aa1791d3c5 | ||
|
|
9f90cce49b | ||
|
|
f281f1f861 | ||
|
|
83b7ae436a | ||
|
|
e5451ea853 | ||
|
|
db4e7667b6 | ||
|
|
f16f1237ab | ||
|
|
b61854732b | ||
|
|
b46135f5d3 | ||
|
|
46dbca9f72 | ||
|
|
2e6b399e41 | ||
|
|
0b0f0ea13c | ||
|
|
8e265c4c17 | ||
|
|
9d240cd0a6 | ||
|
|
0d52160a6f | ||
|
|
bbe9e9db74 |
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
6
node_modules/.package-lock.json
generated
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "danswer",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
183
package-lock.json
generated
Normal file
183
package-lock.json
generated
Normal file
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"name": "onyx",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"react-datepicker": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-datepicker": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
|
||||
"integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.9",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.4.tgz",
|
||||
"integrity": "sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-datepicker": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz",
|
||||
"integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.2",
|
||||
"@types/react": "*",
|
||||
"date-fns": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-datepicker/node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-datepicker": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.6.0.tgz",
|
||||
"integrity": "sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.25.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
package.json
Normal file
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"react-datepicker": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-datepicker": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ const cspHeader = `
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
${
|
||||
process.env.NEXT_PUBLIC_CLOUD_ENABLED === "true"
|
||||
? "upgrade-insecure-requests;"
|
||||
@@ -27,6 +26,17 @@ const nextConfig = {
|
||||
publicRuntimeConfig: {
|
||||
version,
|
||||
},
|
||||
images: {
|
||||
// Used to fetch favicons
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "www.google.com",
|
||||
port: "",
|
||||
pathname: "/s2/favicons/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -44,17 +54,12 @@ const nextConfig = {
|
||||
key: "Referrer-Policy",
|
||||
value: "strict-origin-when-cross-origin",
|
||||
},
|
||||
{
|
||||
key: "X-Frame-Options",
|
||||
value: "DENY",
|
||||
},
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
// Deny all permissions by default
|
||||
value:
|
||||
"accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()",
|
||||
},
|
||||
|
||||
345
web/package-lock.json
generated
345
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
5
web/public/logo.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M27.9998 0L10.8691 7.76944L27.9998 15.5389L45.1305 7.76944L27.9998 0ZM27.9998 40.4611L10.8691 48.2306L27.9998 56L45.1305 48.2306L27.9998 40.4611ZM48.2309 10.8691L56.0001 28.0003L48.2309 45.1314L40.4617 28.0003L48.2309 10.8691ZM15.5385 28.0001L7.76923 10.869L0 28.0001L7.76923 45.1313L15.5385 28.0001Z"
|
||||
fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
7
web/public/web.svg
Normal file
7
web/public/web.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14">
|
||||
<g stroke="#3B82F6" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle fill="transparent" cx="7" cy="7" r="6.5" />
|
||||
<path fill="transparent"
|
||||
d="M.5 7h13m-4 0A11.22 11.22 0 0 1 7 13.5A11.22 11.22 0 0 1 4.5 7A11.22 11.22 0 0 1 7 .5A11.22 11.22 0 0 1 9.5 7Z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -113,7 +113,7 @@ export default function Page() {
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
className="ml-1 w-96 h-9 flex-none rounded-md border border-border bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
className="ml-1 w-96 h-9 flex-none rounded-md border border-border bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
|
||||
{Object.entries(categorizedSources)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||
<div
|
||||
className={`
|
||||
cursor-pointer
|
||||
${isCollapsed ? "h-6" : "pl-4 border-l-2 border-border"}
|
||||
${isCollapsed ? "h-6" : "pl-6 border-l-2 border-border"}
|
||||
`}
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
|
||||
@@ -9,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 "title" for this
|
||||
Starter Message. For example, "Write an
|
||||
email."
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Field
|
||||
name={`starter_messages.${
|
||||
rowIndex * 2 + colIndex
|
||||
}.name`}
|
||||
className="mt-1 w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||
autoComplete="off"
|
||||
placeholder="Enter a name..."
|
||||
onChange={(e: any) => {
|
||||
touchStarterMessages();
|
||||
handleChange(e);
|
||||
}}
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages.${
|
||||
rowIndex * 2 + colIndex
|
||||
}.name`}
|
||||
component="div"
|
||||
className="text-red-500 text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex w-full items-center gap-x-1">
|
||||
<Label
|
||||
small
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
Message
|
||||
</Label>
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<FiInfo size={12} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center">
|
||||
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
|
||||
The actual message to be sent as the initial
|
||||
user message.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Field
|
||||
name={`starter_messages.${
|
||||
rowIndex * 2 + colIndex
|
||||
}.message`}
|
||||
className="mt-1 text-sm w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition min-h-[100px] resize-y"
|
||||
as="textarea"
|
||||
autoComplete="off"
|
||||
placeholder="Enter the message..."
|
||||
onChange={(e: any) => {
|
||||
touchStarterMessages();
|
||||
handleChange(e);
|
||||
}}
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={`starter_messages.${
|
||||
rowIndex * 2 + colIndex
|
||||
}.message`}
|
||||
component="div"
|
||||
className="text-red-500 text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export function AssistantsPageTitle({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
}) {
|
||||
return (
|
||||
<h1
|
||||
className="
|
||||
text-5xl
|
||||
font-bold
|
||||
mb-4
|
||||
text-center
|
||||
text-text-900
|
||||
"
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export function NavigationButton({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
text-default
|
||||
py-2
|
||||
px-4
|
||||
rounded-full
|
||||
border-2
|
||||
border-border
|
||||
hover:bg-hover
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-border
|
||||
focus:ring-opacity-50
|
||||
"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { PersonaCategory as PersonaCategoryType } from "../admin/assistants/interfaces";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export default function PersonaCategory({
|
||||
personaCategory,
|
||||
}: {
|
||||
personaCategory: PersonaCategoryType;
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-help">
|
||||
<Badge variant="purple">{personaCategory.name}</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{personaCategory.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
import Cookies from "js-cookie";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import {
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Persona,
|
||||
PersonaCategory as PersonaCategoryType,
|
||||
} from "@/app/admin/assistants/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { User } from "@/lib/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { FiList, FiMinus, FiPlus } from "react-icons/fi";
|
||||
import { AssistantsPageTitle } from "../AssistantsPageTitle";
|
||||
import {
|
||||
addAssistantToList,
|
||||
removeAssistantFromList,
|
||||
} from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AssistantTools } from "../ToolsDisplay";
|
||||
import { classifyAssistants } from "@/lib/assistants/utils";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import PersonaCategory from "../PersonaCategory";
|
||||
import { useCategories } from "@/lib/hooks";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export function AssistantGalleryCard({
|
||||
onlyAssistant,
|
||||
assistant,
|
||||
user,
|
||||
setPopup,
|
||||
selectedAssistant,
|
||||
}: {
|
||||
onlyAssistant: boolean;
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
setPopup: (popup: PopupSpec) => void;
|
||||
selectedAssistant: boolean;
|
||||
}) {
|
||||
const { data: categories } = useCategories();
|
||||
const { refreshUser } = useUser();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={assistant.id}
|
||||
className="
|
||||
bg-background-emphasis
|
||||
rounded-lg
|
||||
shadow-md
|
||||
p-4
|
||||
"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
<h2
|
||||
className="
|
||||
text-xl
|
||||
font-semibold
|
||||
my-auto
|
||||
ml-2
|
||||
text-strong
|
||||
line-clamp-2
|
||||
"
|
||||
>
|
||||
{assistant.name}
|
||||
</h2>
|
||||
{user && (
|
||||
<div className="ml-auto">
|
||||
{selectedAssistant ? (
|
||||
<Button
|
||||
className="
|
||||
mr-2
|
||||
my-auto
|
||||
bg-background-700
|
||||
hover:bg-background-600
|
||||
"
|
||||
icon={FiMinus}
|
||||
onClick={async () => {
|
||||
if (onlyAssistant) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await removeAssistantFromList(assistant.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
Deselect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="
|
||||
mr-2
|
||||
my-auto
|
||||
bg-accent
|
||||
hover:bg-accent-hover
|
||||
"
|
||||
icon={FiPlus}
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(assistant.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="submit"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm mt-2">{assistant.description}</p>
|
||||
<p className="text-subtle text-sm my-2">
|
||||
Author: {assistant.owner?.email || "Onyx"}
|
||||
</p>
|
||||
{assistant.tools.length > 0 && (
|
||||
<AssistantTools list assistant={assistant} />
|
||||
)}
|
||||
{assistant.category_id && categories && (
|
||||
<PersonaCategory
|
||||
personaCategory={
|
||||
categories?.find(
|
||||
(category: PersonaCategoryType) =>
|
||||
category.id === assistant.category_id
|
||||
)!
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function AssistantsGallery() {
|
||||
const { assistants } = useAssistants();
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: categories } = useCategories();
|
||||
const router = useRouter();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
||||
|
||||
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
|
||||
user,
|
||||
assistants
|
||||
);
|
||||
|
||||
const defaultAssistants = assistants
|
||||
.filter((assistant) => assistant.is_default_persona)
|
||||
.filter(
|
||||
(assistant) =>
|
||||
(assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
assistant.description
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())) &&
|
||||
(selectedCategory === null ||
|
||||
selectedCategory === assistant.category_id)
|
||||
);
|
||||
|
||||
const nonDefaultAssistants = assistants
|
||||
.filter((assistant) => !assistant.is_default_persona)
|
||||
.filter(
|
||||
(assistant) =>
|
||||
(assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
assistant.description
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())) &&
|
||||
(selectedCategory === null ||
|
||||
selectedCategory === assistant.category_id)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Assistant Gallery</AssistantsPageTitle>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-4 mb-6">
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/new")}
|
||||
variant="default"
|
||||
className="p-6 text-base"
|
||||
icon={FiPlus}
|
||||
>
|
||||
Create New Assistant
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/mine")}
|
||||
variant="outline"
|
||||
className="text-base py-6"
|
||||
icon={FiList}
|
||||
>
|
||||
Your Assistants
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-6">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search assistants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="
|
||||
w-full
|
||||
py-3
|
||||
px-4
|
||||
pl-10
|
||||
text-lg
|
||||
border-2
|
||||
border-background-strong
|
||||
rounded-full
|
||||
bg-background-50
|
||||
text-text-700
|
||||
placeholder-text-400
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-primary-500
|
||||
focus:border-transparent
|
||||
transition duration-300 ease-in-out
|
||||
"
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
className="h-5 w-5 text-text-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{categories && categories?.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<Select
|
||||
value={selectedCategory?.toString() || "all"}
|
||||
onValueChange={(value) =>
|
||||
setSelectedCategory(value === "all" ? null : parseInt(value))
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="
|
||||
w-[240px]
|
||||
bg-background
|
||||
border-2
|
||||
border-background-strong
|
||||
text-text-500
|
||||
rounded-lg
|
||||
shadow-sm
|
||||
hover:bg-background-emphasis
|
||||
hover:border-primary-500/50
|
||||
hover:text-primary-500
|
||||
transition-all
|
||||
duration-200
|
||||
"
|
||||
>
|
||||
<SelectValue placeholder="Filter by category..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background border-background-strong">
|
||||
<SelectGroup>
|
||||
<SelectLabel className="text-sm font-medium text-text-400">
|
||||
Categories
|
||||
</SelectLabel>
|
||||
<SelectItem
|
||||
value="all"
|
||||
className="cursor-pointer hover:bg-background-emphasis"
|
||||
>
|
||||
All Categories
|
||||
</SelectItem>
|
||||
{categories.map((category) => (
|
||||
<SelectItem
|
||||
key={category.id}
|
||||
value={category.id.toString()}
|
||||
className="cursor-pointer hover:bg-background-emphasis"
|
||||
>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{defaultAssistants.length == 0 &&
|
||||
nonDefaultAssistants.length == 0 &&
|
||||
assistants.length != 0 && (
|
||||
<div className="text-text-500">
|
||||
No assistants found for this search
|
||||
</div>
|
||||
)}
|
||||
|
||||
{defaultAssistants.length > 0 && (
|
||||
<>
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-text-900">
|
||||
Default Assistants
|
||||
</h2>
|
||||
|
||||
<h3 className="text-lg text-text-500">
|
||||
These are assistant created by your admins are and preferred.
|
||||
</h3>
|
||||
</section>
|
||||
<div
|
||||
className="
|
||||
w-full
|
||||
grid
|
||||
grid-cols-2
|
||||
gap-4
|
||||
py-2
|
||||
"
|
||||
>
|
||||
{defaultAssistants.map((assistant) => (
|
||||
<AssistantGalleryCard
|
||||
onlyAssistant={visibleAssistants.length === 1}
|
||||
selectedAssistant={visibleAssistants.includes(assistant)}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{nonDefaultAssistants.length > 0 && (
|
||||
<section className="mt-12 mb-8 flex flex-col gap-y-2">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-2xl font-semibold text-text-900">
|
||||
Other Assistants
|
||||
</h2>
|
||||
<h3 className="text-lg text-text-500">
|
||||
These are community-contributed assistants.
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="
|
||||
w-full
|
||||
grid
|
||||
grid-cols-2
|
||||
gap-4
|
||||
py-2
|
||||
"
|
||||
>
|
||||
{nonDefaultAssistants.map((assistant) => (
|
||||
<AssistantGalleryCard
|
||||
onlyAssistant={visibleAssistants.length === 1}
|
||||
selectedAssistant={visibleAssistants.includes(assistant)}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import SidebarWrapper from "../SidebarWrapper";
|
||||
import { AssistantsGallery } from "./AssistantsGallery";
|
||||
|
||||
export default function WrappedAssistantsGallery({
|
||||
toggleSidebar,
|
||||
}: {
|
||||
toggleSidebar: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper page="chat" initiallyToggled={toggleSidebar}>
|
||||
<AssistantsGallery />
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import WrappedAssistantsGallery from "./WrappedAssistantsGallery";
|
||||
import { cookies } from "next/headers";
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
|
||||
export default async function GalleryPage(props: {
|
||||
searchParams: Promise<{ [key: string]: string }>;
|
||||
}) {
|
||||
noStore();
|
||||
|
||||
const searchParams = await props.searchParams;
|
||||
const requestCookies = await cookies();
|
||||
const data = await fetchChatData(searchParams);
|
||||
|
||||
if ("redirect" in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
defaultAssistantId,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
>
|
||||
{shouldShowWelcomeModal && (
|
||||
<WelcomeModal user={user} requestCookies={requestCookies} />
|
||||
)}
|
||||
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<WrappedAssistantsGallery toggleSidebar={toggleSidebar} />
|
||||
</ChatProvider>
|
||||
);
|
||||
}
|
||||
255
web/src/app/assistants/mine/AssistantCard.tsx
Normal file
255
web/src/app/assistants/mine/AssistantCard.tsx
Normal 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;
|
||||
188
web/src/app/assistants/mine/AssistantModal.tsx
Normal file
188
web/src/app/assistants/mine/AssistantModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { MinimalUserSnapshot, User } from "@/lib/types";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
FiBarChart,
|
||||
FiEdit2,
|
||||
FiList,
|
||||
FiMinus,
|
||||
FiMoreHorizontal,
|
||||
FiPlus,
|
||||
FiShare2,
|
||||
FiTrash,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
addAssistantToList,
|
||||
removeAssistantFromList,
|
||||
updateUserAssistantList,
|
||||
} from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { DefaultPopover } from "@/components/popover/DefaultPopover";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AssistantsPageTitle } from "../AssistantsPageTitle";
|
||||
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
|
||||
import { AssistantSharingModal } from "./AssistantSharingModal";
|
||||
import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
|
||||
import { DragHandle } from "@/components/table/DragHandle";
|
||||
import {
|
||||
deletePersona,
|
||||
togglePersonaPublicStatus,
|
||||
} from "@/app/admin/assistants/lib";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantModal";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
|
||||
function DraggableAssistantListItem({ ...props }: any) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: props.assistant.id.toString() });
|
||||
|
||||
const style = {
|
||||
transform: transform
|
||||
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
|
||||
: undefined,
|
||||
transition,
|
||||
opacity: isDragging ? 0.9 : 1,
|
||||
zIndex: isDragging ? 1000 : "auto",
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="flex mt-2 items-center">
|
||||
<div {...attributes} {...listeners} className="mr-2 cursor-grab">
|
||||
<DragHandle />
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<AssistantListItem isDragging={isDragging} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssistantListItem({
|
||||
assistant,
|
||||
user,
|
||||
allUsers,
|
||||
isVisible,
|
||||
setPopup,
|
||||
deleteAssistant,
|
||||
shareAssistant,
|
||||
isDragging,
|
||||
onlyAssistant,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
allUsers: MinimalUserSnapshot[];
|
||||
isVisible: boolean;
|
||||
deleteAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
shareAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
isDragging?: boolean;
|
||||
onlyAssistant: boolean;
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const router = useRouter();
|
||||
const [showSharingModal, setShowSharingModal] = useState(false);
|
||||
|
||||
const isEnterpriseEnabled = usePaidEnterpriseFeaturesEnabled();
|
||||
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
|
||||
const { isAdmin } = useUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AssistantSharingModal
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
allUsers={allUsers}
|
||||
onClose={() => {
|
||||
setShowSharingModal(false);
|
||||
router.refresh();
|
||||
}}
|
||||
show={showSharingModal}
|
||||
/>
|
||||
<div
|
||||
className={`rounded-lg px-4 py-6 transition-all duration-900 hover:bg-background-125 ${
|
||||
isDragging && "bg-background-125"
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
|
||||
<h2 className="ml-6 w-fit flex-grow space-y-3 text-start flex text-xl font-semibold line-clamp-2 text-gray-800">
|
||||
{assistant.name}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-none items-center space-x-4">
|
||||
<div className="flex mr-20 flex-wrap items-center gap-x-4">
|
||||
{assistant.tools.length > 0 && (
|
||||
<p className="text-base flex w-fit text-subtle">
|
||||
{assistant.tools.length} tool
|
||||
{assistant.tools.length > 1 && "s"}
|
||||
</p>
|
||||
)}
|
||||
<AssistantSharedStatusDisplay
|
||||
size="md"
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isOwnedByUser ? (
|
||||
<Link
|
||||
href={`/assistants/edit/${assistant.id}`}
|
||||
className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200"
|
||||
title="Edit assistant"
|
||||
>
|
||||
<FiEdit2 size={20} className="text-text-900" />
|
||||
</Link>
|
||||
) : (
|
||||
<CustomTooltip
|
||||
showTick
|
||||
content="You don't have permission to edit this assistant"
|
||||
>
|
||||
<div className="p-2 cursor-not-allowed opacity-50 rounded-full hover:bg-gray-100 transition-colors duration-200">
|
||||
<FiEdit2 size={20} className="text-text-900" />
|
||||
</div>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
|
||||
<DefaultPopover
|
||||
content={
|
||||
<div className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200 cursor-pointer">
|
||||
<FiMoreHorizontal size={20} className="text-text-900" />
|
||||
</div>
|
||||
}
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
>
|
||||
{[
|
||||
isVisible ? (
|
||||
<button
|
||||
key="remove"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={async () => {
|
||||
if (onlyAssistant) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await removeAssistantFromList(
|
||||
assistant.id
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiX size={18} className="text-text-800" />{" "}
|
||||
{isOwnedByUser ? "Hide" : "Remove"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
key="add"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(assistant.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiPlus size={18} className="text-text-800" /> Add
|
||||
</button>
|
||||
),
|
||||
|
||||
(isOwnedByUser || isAdmin) && isEnterpriseEnabled ? (
|
||||
<button
|
||||
key="view-stats"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={() =>
|
||||
router.push(`/assistants/stats/${assistant.id}`)
|
||||
}
|
||||
>
|
||||
<FiBarChart size={18} /> View Stats
|
||||
</button>
|
||||
) : null,
|
||||
isOwnedByUser ? (
|
||||
<button
|
||||
key="delete"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left text-red-600"
|
||||
onClick={() => deleteAssistant(assistant)}
|
||||
>
|
||||
<FiTrash size={18} /> Delete
|
||||
</button>
|
||||
) : null,
|
||||
isOwnedByUser ? (
|
||||
<button
|
||||
key="visibility"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={() => shareAssistant(assistant)}
|
||||
>
|
||||
{assistant.is_public ? (
|
||||
<FiMinus size={18} className="text-text-800" />
|
||||
) : (
|
||||
<FiPlus size={18} className="text-text-800" />
|
||||
)}{" "}
|
||||
Make {assistant.is_public ? "Private" : "Public"}
|
||||
</button>
|
||||
) : null,
|
||||
!assistant.is_public ? (
|
||||
<button
|
||||
key="share"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={(e) => {
|
||||
setShowSharingModal(true);
|
||||
}}
|
||||
>
|
||||
<FiShare2 size={18} className="text-text-800" /> Share
|
||||
</button>
|
||||
) : null,
|
||||
]}
|
||||
</DefaultPopover>
|
||||
</div>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export function AssistantsList() {
|
||||
const {
|
||||
assistants,
|
||||
ownedButHiddenAssistants,
|
||||
finalAssistants,
|
||||
refreshAssistants,
|
||||
} = useAssistants();
|
||||
|
||||
const [currentlyVisibleAssistants, setCurrentlyVisibleAssistants] =
|
||||
useState(finalAssistants);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentlyVisibleAssistants(finalAssistants);
|
||||
}, [finalAssistants]);
|
||||
|
||||
const allAssistantIds = assistants.map((assistant) =>
|
||||
assistant.id.toString()
|
||||
);
|
||||
|
||||
const [deletingPersona, setDeletingPersona] = useState<Persona | null>(null);
|
||||
const [makePublicPersona, setMakePublicPersona] = useState<Persona | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { refreshUser, user } = useUser();
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
const { data: users } = useSWR<MinimalUserSnapshot[]>(
|
||||
"/api/users",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
async function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = currentlyVisibleAssistants.findIndex(
|
||||
(item) => item.id.toString() === active.id
|
||||
);
|
||||
const newIndex = currentlyVisibleAssistants.findIndex(
|
||||
(item) => item.id.toString() === over.id
|
||||
);
|
||||
const updatedAssistants = arrayMove(
|
||||
currentlyVisibleAssistants,
|
||||
oldIndex,
|
||||
newIndex
|
||||
);
|
||||
|
||||
setCurrentlyVisibleAssistants(updatedAssistants);
|
||||
await updateUserAssistantList(updatedAssistants.map((a) => a.id));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
{deletingPersona && (
|
||||
<DeleteEntityModal
|
||||
entityType="Assistant"
|
||||
entityName={deletingPersona.name}
|
||||
onClose={() => setDeletingPersona(null)}
|
||||
onSubmit={async () => {
|
||||
const success = await deletePersona(deletingPersona.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${deletingPersona.name}" has been deleted.`,
|
||||
type: "success",
|
||||
});
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${deletingPersona.name}" could not be deleted.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
setDeletingPersona(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{makePublicPersona && (
|
||||
<MakePublicAssistantModal
|
||||
isPublic={makePublicPersona.is_public}
|
||||
onClose={() => setMakePublicPersona(null)}
|
||||
onShare={async (newPublicStatus: boolean) => {
|
||||
await togglePersonaPublicStatus(
|
||||
makePublicPersona.id,
|
||||
newPublicStatus
|
||||
);
|
||||
await refreshAssistants();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Your Assistants</AssistantsPageTitle>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-4 mb-8">
|
||||
<Button
|
||||
variant="default"
|
||||
className="p-6 text-base"
|
||||
onClick={() => router.push("/assistants/new")}
|
||||
icon={FiPlus}
|
||||
>
|
||||
Create New Assistant
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/gallery")}
|
||||
variant="outline"
|
||||
className="text-base py-6"
|
||||
icon={FiList}
|
||||
>
|
||||
Assistant Gallery
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-2 text-text-900">
|
||||
Active Assistants
|
||||
</h2>
|
||||
|
||||
<h3 className="text-lg text-text-500">
|
||||
The order the assistants appear below will be the order they appear in
|
||||
the Assistants dropdown. The first assistant listed will be your
|
||||
default assistant when you start a new chat. Drag and drop to reorder.
|
||||
</h3>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={currentlyVisibleAssistants.map((a) => a.id.toString())}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="w-full items-center py-4">
|
||||
{currentlyVisibleAssistants.map((assistant, index) => (
|
||||
<DraggableAssistantListItem
|
||||
onlyAssistant={currentlyVisibleAssistants.length === 1}
|
||||
deleteAssistant={setDeletingPersona}
|
||||
shareAssistant={setMakePublicPersona}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
allAssistantIds={allAssistantIds}
|
||||
allUsers={users || []}
|
||||
isVisible
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{ownedButHiddenAssistants.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<h3 className="text-xl font-bold mb-4">Your Hidden Assistants</h3>
|
||||
|
||||
<h3 className="text-lg text-text-500">
|
||||
Assistants you've created that aren't currently visible
|
||||
in the Assistants selector.
|
||||
</h3>
|
||||
|
||||
<div className="w-full p-4">
|
||||
{ownedButHiddenAssistants.map((assistant, index) => (
|
||||
<AssistantListItem
|
||||
onlyAssistant={currentlyVisibleAssistants.length === 1}
|
||||
deleteAssistant={setDeletingPersona}
|
||||
shareAssistant={setMakePublicPersona}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
allUsers={users || []}
|
||||
isVisible={false}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
import { AssistantsList } from "./AssistantsList";
|
||||
import SidebarWrapper from "../SidebarWrapper";
|
||||
|
||||
export default function WrappedAssistantsMine({
|
||||
initiallyToggled,
|
||||
}: {
|
||||
initiallyToggled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper page="chat" initiallyToggled={initiallyToggled}>
|
||||
<AssistantsList />
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import WrappedAssistantsMine from "./WrappedAssistantsMine";
|
||||
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { cookies } from "next/headers";
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
|
||||
export default async function GalleryPage(props: {
|
||||
searchParams: Promise<{ [key: string]: string }>;
|
||||
}) {
|
||||
noStore();
|
||||
const requestCookies = await cookies();
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const data = await fetchChatData(searchParams);
|
||||
|
||||
if ("redirect" in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
llmProviders,
|
||||
defaultAssistantId,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
tags,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
>
|
||||
{shouldShowWelcomeModal && (
|
||||
<WelcomeModal user={user} requestCookies={requestCookies} />
|
||||
)}
|
||||
|
||||
<InstantSSRAutoRefresh />
|
||||
<WrappedAssistantsMine initiallyToggled={toggleSidebar} />
|
||||
</ChatProvider>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
106
web/src/app/auth/login/LoginPage.tsx
Normal file
106
web/src/app/auth/login/LoginPage.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { AuthTypeMetadata } from "@/lib/userSS";
|
||||
import { LoginText } from "./LoginText";
|
||||
import Link from "next/link";
|
||||
import { SignInButton } from "./SignInButton";
|
||||
import { EmailPasswordForm } from "./EmailPasswordForm";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import Title from "@/components/ui/title";
|
||||
import { useSendAuthRequiredMessage } from "@/lib/extension/utils";
|
||||
|
||||
export default function LoginPage({
|
||||
authUrl,
|
||||
authTypeMetadata,
|
||||
nextUrl,
|
||||
searchParams,
|
||||
showPageRedirect,
|
||||
}: {
|
||||
authUrl: string | null;
|
||||
authTypeMetadata: AuthTypeMetadata | null;
|
||||
nextUrl: string | null;
|
||||
searchParams:
|
||||
| {
|
||||
[key: string]: string | string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
showPageRedirect?: boolean;
|
||||
}) {
|
||||
useSendAuthRequiredMessage();
|
||||
return (
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
{authUrl && authTypeMetadata && (
|
||||
<>
|
||||
<h2 className="text-center text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</h2>
|
||||
|
||||
<SignInButton
|
||||
authorizeUrl={authUrl}
|
||||
authType={authTypeMetadata?.authType}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "cloud" && (
|
||||
<div className="mt-4 w-full justify-center">
|
||||
<div className="flex items-center w-full my-4">
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
<span className="px-4 text-gray-500">or</span>
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
</div>
|
||||
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
|
||||
|
||||
<div className="flex mt-4 justify-between">
|
||||
<Link
|
||||
href={`/auth/signup${
|
||||
searchParams?.next ? `?next=${searchParams.next}` : ""
|
||||
}`}
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
|
||||
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Reset Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "basic" && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</Title>
|
||||
</div>
|
||||
<EmailPasswordForm nextUrl={nextUrl} />
|
||||
<div className="flex flex-col gap-y-2 items-center"></div>
|
||||
</>
|
||||
)}
|
||||
{showPageRedirect && (
|
||||
<p className="text-center mt-4">
|
||||
Don't have an account?{" "}
|
||||
<span
|
||||
onClick={() => {
|
||||
if (typeof window !== "undefined" && window.top) {
|
||||
window.top.location.href = "/auth/signup";
|
||||
} else {
|
||||
window.location.href = "/auth/signup";
|
||||
}
|
||||
}}
|
||||
className="text-link font-medium cursor-pointer"
|
||||
>
|
||||
Create an account
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function SignInButton({
|
||||
|
||||
return (
|
||||
<a
|
||||
className="mx-auto mt-6 py-3 w-full text-text-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
|
||||
className="mx-auto mb-4 mt-6 py-3 w-full text-text-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
|
||||
href={finalAuthorizeUrl}
|
||||
>
|
||||
{button}
|
||||
|
||||
@@ -7,18 +7,8 @@ import {
|
||||
AuthTypeMetadata,
|
||||
} from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
import { SignInButton } from "./SignInButton";
|
||||
import { EmailPasswordForm } from "./EmailPasswordForm";
|
||||
import Title from "@/components/ui/title";
|
||||
import Text from "@/components/ui/text";
|
||||
import Link from "next/link";
|
||||
import { LoginText } from "./LoginText";
|
||||
import { getSecondsUntilExpiration } from "@/lib/time";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { useContext } from "react";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
const Page = async (props: {
|
||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
@@ -49,13 +39,7 @@ const Page = async (props: {
|
||||
}
|
||||
|
||||
// if user is already logged in, take them to the main app page
|
||||
const secondsTillExpiration = getSecondsUntilExpiration(currentUser);
|
||||
if (
|
||||
currentUser &&
|
||||
currentUser.is_active &&
|
||||
!currentUser.is_anonymous_user &&
|
||||
(secondsTillExpiration === null || secondsTillExpiration > 0)
|
||||
) {
|
||||
if (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
|
||||
if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) {
|
||||
return redirect("/auth/waiting-on-verification");
|
||||
}
|
||||
@@ -83,55 +67,12 @@ const Page = async (props: {
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
{authUrl && authTypeMetadata && (
|
||||
<>
|
||||
<h2 className="text-center text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</h2>
|
||||
|
||||
<SignInButton
|
||||
authorizeUrl={authUrl}
|
||||
authType={authTypeMetadata?.authType}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "cloud" && (
|
||||
<div className="mt-4 w-full justify-center">
|
||||
<div className="flex items-center w-full my-4">
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
<span className="px-4 text-gray-500">or</span>
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
|
||||
|
||||
<div className="flex mt-4 justify-between">
|
||||
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Reset Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "basic" && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</Title>
|
||||
</div>
|
||||
<EmailPasswordForm nextUrl={nextUrl} />
|
||||
<div className="flex flex-col gap-y-2 items-center"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<LoginPage
|
||||
authUrl={authUrl}
|
||||
authTypeMetadata={authTypeMetadata}
|
||||
nextUrl={nextUrl!}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</AuthFlowContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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";
|
||||
180
web/src/app/chat/documentSidebar/DocumentResults.tsx
Normal file
180
web/src/app/chat/documentSidebar/DocumentResults.tsx
Normal 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";
|
||||
@@ -111,27 +111,28 @@ export function InputBarPreview({
|
||||
z-0
|
||||
"
|
||||
>
|
||||
<FiLoader className="animate-spin text-white" />
|
||||
<FiLoader size={12} className="animate-spin text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
flex
|
||||
items-center
|
||||
p-2
|
||||
px-2
|
||||
bg-hover
|
||||
border
|
||||
gap-x-1.5
|
||||
border-border
|
||||
rounded-md
|
||||
box-border
|
||||
h-10
|
||||
h-8
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
className="
|
||||
w-6
|
||||
h-6
|
||||
w-5
|
||||
h-5
|
||||
bg-document
|
||||
flex
|
||||
items-center
|
||||
@@ -139,33 +140,31 @@ export function InputBarPreview({
|
||||
rounded-md
|
||||
"
|
||||
>
|
||||
<FiFileText className="w-4 h-4 text-white" />
|
||||
<FiFileText size={12} className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
ref={fileNameRef}
|
||||
className={`font-medium text-sm line-clamp-1 break-all ellipses max-w-48`}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start">
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
ref={fileNameRef}
|
||||
className={`font-medium text-sm line-clamp-1 break-all ellipses max-w-48`}
|
||||
>
|
||||
{file.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start">
|
||||
{file.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="
|
||||
cursor-pointer
|
||||
border-none
|
||||
bg-hover
|
||||
p-1
|
||||
rounded-full
|
||||
z-10
|
||||
"
|
||||
|
||||
@@ -24,25 +24,26 @@ export function DocumentPreview({
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
${alignBubble && "w-64"}
|
||||
${alignBubble && "min-w-52 max-w-48"}
|
||||
flex
|
||||
items-center
|
||||
p-3
|
||||
bg-hover
|
||||
bg-hover-light/50
|
||||
border
|
||||
border-border
|
||||
rounded-lg
|
||||
box-border
|
||||
h-20
|
||||
py-4
|
||||
h-12
|
||||
hover:shadow-sm
|
||||
transition-all
|
||||
px-2
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
className="
|
||||
w-14
|
||||
h-14
|
||||
w-8
|
||||
h-8
|
||||
bg-document
|
||||
flex
|
||||
items-center
|
||||
@@ -53,10 +54,10 @@ export function DocumentPreview({
|
||||
hover:bg-document-dark
|
||||
"
|
||||
>
|
||||
<FiFileText className="w-7 h-7 text-white" />
|
||||
<FiFileText className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 flex-grow">
|
||||
<div className="ml-2 h-8 flex flex-col flex-grow">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -74,7 +75,7 @@ export function DocumentPreview({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="text-subtle text-xs mt-1">Document</div>
|
||||
<div className="text-subtle text-xs">Document</div>
|
||||
</div>
|
||||
{open && (
|
||||
<button
|
||||
|
||||
148
web/src/app/chat/folders/FolderDropdown.tsx
Normal file
148
web/src/app/chat/folders/FolderDropdown.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
145
web/src/app/chat/input/LLMPopover.tsx
Normal file
145
web/src/app/chat/input/LLMPopover.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
244
web/src/app/chat/input/SimplifiedChatInputBar.tsx
Normal file
244
web/src/app/chat/input/SimplifiedChatInputBar.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { FiPlusCircle } from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import {
|
||||
InputBarPreview,
|
||||
InputBarPreviewImageProvider,
|
||||
} from "../files/InputBarPreview";
|
||||
import { OpenAIIcon, SendIcon } from "@/components/icons/icons";
|
||||
import { HorizontalSourceSelector } from "@/components/search/filtering/HorizontalSourceSelector";
|
||||
import { Tag } from "@/lib/types";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
interface ChatInputBarProps {
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
onSubmit: () => void;
|
||||
files: FileDescriptor[];
|
||||
setFiles: (files: FileDescriptor[]) => void;
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
filterManager?: FilterManager;
|
||||
existingSources: string[];
|
||||
availableDocumentSets: { name: string }[];
|
||||
availableTags: Tag[];
|
||||
}
|
||||
|
||||
export function SimplifiedChatInputBar({
|
||||
message,
|
||||
setMessage,
|
||||
onSubmit,
|
||||
files,
|
||||
setFiles,
|
||||
handleFileUpload,
|
||||
textAreaRef,
|
||||
filterManager,
|
||||
existingSources,
|
||||
availableDocumentSets,
|
||||
availableTags,
|
||||
}: ChatInputBarProps) {
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "0px";
|
||||
textarea.style.height = `${Math.min(
|
||||
textarea.scrollHeight,
|
||||
MAX_INPUT_HEIGHT
|
||||
)}px`;
|
||||
}
|
||||
}, [message, textAreaRef]);
|
||||
|
||||
const handlePaste = (event: React.ClipboardEvent) => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (items) {
|
||||
const pastedFiles = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].kind === "file") {
|
||||
const file = items[i].getAsFile();
|
||||
if (file) pastedFiles.push(file);
|
||||
}
|
||||
}
|
||||
if (pastedFiles.length > 0) {
|
||||
event.preventDefault();
|
||||
handleFileUpload(pastedFiles);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const text = event.target.value;
|
||||
setMessage(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="onyx-chat-input"
|
||||
className="
|
||||
w-full
|
||||
relative
|
||||
mx-auto
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
opacity-100
|
||||
w-full
|
||||
h-fit
|
||||
flex
|
||||
flex-col
|
||||
border
|
||||
border-[#E5E7EB]
|
||||
rounded-lg
|
||||
relative
|
||||
text-text-chatbar
|
||||
bg-background-chatbar
|
||||
[&:has(textarea:focus)]::ring-1
|
||||
[&:has(textarea:focus)]::ring-black
|
||||
"
|
||||
>
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-x-2 px-2 pt-2">
|
||||
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
||||
{files.map((file) => (
|
||||
<div className="flex-none" key={file.id}>
|
||||
{file.type === ChatFileType.IMAGE ? (
|
||||
<InputBarPreviewImageProvider
|
||||
file={file}
|
||||
onDelete={() => {
|
||||
setFiles(
|
||||
files.filter(
|
||||
(fileInFilter) => fileInFilter.id !== file.id
|
||||
)
|
||||
);
|
||||
}}
|
||||
isUploading={file.isUploading || false}
|
||||
/>
|
||||
) : (
|
||||
<InputBarPreview
|
||||
file={file}
|
||||
onDelete={() => {
|
||||
setFiles(
|
||||
files.filter(
|
||||
(fileInFilter) => fileInFilter.id !== file.id
|
||||
)
|
||||
);
|
||||
}}
|
||||
isUploading={file.isUploading || false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
onPaste={handlePaste}
|
||||
onChange={handleInputChange}
|
||||
ref={textAreaRef}
|
||||
className={`
|
||||
m-0
|
||||
w-full
|
||||
shrink
|
||||
resize-none
|
||||
rounded-lg
|
||||
border-0
|
||||
bg-background-chatbar
|
||||
placeholder:text-text-chatbar-subtle
|
||||
${
|
||||
textAreaRef.current &&
|
||||
textAreaRef.current.scrollHeight > MAX_INPUT_HEIGHT
|
||||
? "overflow-y-auto mt-2"
|
||||
: ""
|
||||
}
|
||||
whitespace-normal
|
||||
break-word
|
||||
overscroll-contain
|
||||
outline-none
|
||||
placeholder-subtle
|
||||
resize-none
|
||||
px-5
|
||||
py-4
|
||||
h-14
|
||||
`}
|
||||
autoFocus
|
||||
style={{ scrollbarWidth: "thin" }}
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
placeholder="Ask me anything..."
|
||||
value={message}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!(event.nativeEvent as any).isComposing
|
||||
) {
|
||||
event.preventDefault();
|
||||
if (message) {
|
||||
onSubmit();
|
||||
}
|
||||
}
|
||||
}}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div className="flex items-center space-x-3 mr-12 px-4 pb-2">
|
||||
<ChatInputOption
|
||||
flexPriority="stiff"
|
||||
name="File"
|
||||
Icon={FiPlusCircle}
|
||||
onClick={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true; // Allow multiple files
|
||||
input.onchange = (event: any) => {
|
||||
const selectedFiles = Array.from(
|
||||
event?.target?.files || []
|
||||
) as File[];
|
||||
if (selectedFiles.length > 0) {
|
||||
handleFileUpload(selectedFiles);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
/>
|
||||
|
||||
{filterManager && (
|
||||
<HorizontalSourceSelector
|
||||
timeRange={filterManager.timeRange}
|
||||
setTimeRange={filterManager.setTimeRange}
|
||||
selectedSources={filterManager.selectedSources}
|
||||
setSelectedSources={filterManager.setSelectedSources}
|
||||
selectedDocumentSets={filterManager.selectedDocumentSets}
|
||||
setSelectedDocumentSets={filterManager.setSelectedDocumentSets}
|
||||
selectedTags={filterManager.selectedTags}
|
||||
setSelectedTags={filterManager.setSelectedTags}
|
||||
existingSources={existingSources}
|
||||
availableDocumentSets={availableDocumentSets}
|
||||
availableTags={availableTags}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-2 mobile:right-4 desktop:right-4">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (message) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SendIcon
|
||||
size={28}
|
||||
className={`text-emphasis text-white p-1 rounded-full ${
|
||||
message ? "bg-submit-background" : "bg-disabled-submit-background"
|
||||
} `}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
web/src/app/chat/layout.tsx
Normal file
62
web/src/app/chat/layout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Citation } from "@/components/search/results/Citation";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { LoadedOnyxDocument, OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { getSourceMetadata, SOURCE_METADATA_MAP } from "@/lib/sources";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import React, { memo } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { SlackIcon } from "@/components/icons/icons";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
|
||||
export const MemoizedAnchor = memo(
|
||||
({
|
||||
@@ -23,22 +20,28 @@ export const MemoizedAnchor = memo(
|
||||
const match = value.match(/\[(\d+)\]/);
|
||||
if (match) {
|
||||
const index = parseInt(match[1], 10) - 1;
|
||||
const associatedDoc = docs && docs[index];
|
||||
const associatedDoc = docs?.[index];
|
||||
if (!associatedDoc) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const url = associatedDoc?.link
|
||||
? new URL(associatedDoc.link).origin + "/favicon.ico"
|
||||
: "";
|
||||
|
||||
const icon =
|
||||
(associatedDoc && (
|
||||
<SourceIcon sourceType={associatedDoc?.source_type} iconSize={18} />
|
||||
)) ||
|
||||
null;
|
||||
let icon: React.ReactNode = null;
|
||||
if (associatedDoc.source_type === "web") {
|
||||
icon = <WebResultIcon url={associatedDoc.link} />;
|
||||
} else {
|
||||
icon = (
|
||||
<SourceIcon sourceType={associatedDoc.source_type} iconSize={18} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MemoizedLink
|
||||
updatePresentingDocument={updatePresentingDocument}
|
||||
document={{ ...associatedDoc, icon, url }}
|
||||
document={{
|
||||
...associatedDoc,
|
||||
icon,
|
||||
url: associatedDoc.link,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MemoizedLink>
|
||||
@@ -66,7 +69,6 @@ export const MemoizedLink = memo((props: any) => {
|
||||
<Citation
|
||||
url={document?.url}
|
||||
icon={document?.icon as React.ReactNode}
|
||||
link={rest?.href}
|
||||
document={document as LoadedOnyxDocument}
|
||||
updatePresentingDocument={updatePresentingDocument}
|
||||
>
|
||||
|
||||
@@ -19,11 +19,7 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
OnyxDocument,
|
||||
FilteredOnyxDocument,
|
||||
LoadedOnyxDocument,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { OnyxDocument, FilteredOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { SearchSummary } from "./SearchSummary";
|
||||
|
||||
import { SkippedSearch } from "./SkippedSearch";
|
||||
@@ -111,7 +107,6 @@ function FileDisplay({
|
||||
<div key={file.id} className="w-fit">
|
||||
<DocumentPreview
|
||||
fileName={file.name || file.id}
|
||||
maxWidth="max-w-64"
|
||||
alignBubble={alignBubble}
|
||||
/>
|
||||
</div>
|
||||
@@ -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 &&
|
||||
|
||||
@@ -4,12 +4,15 @@ import {
|
||||
} from "@/components/BasicClickable";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
import { Hoverable } from "@/components/Hoverable";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FiCheck, FiEdit2, FiSearch, FiX } from "react-icons/fi";
|
||||
|
||||
@@ -45,11 +48,15 @@ export function SearchSummary({
|
||||
query,
|
||||
finished,
|
||||
handleSearchQueryEdit,
|
||||
docs,
|
||||
toggleDocumentSelection,
|
||||
}: {
|
||||
index: number;
|
||||
finished: boolean;
|
||||
query: string;
|
||||
handleSearchQueryEdit?: (query: string) => void;
|
||||
docs: OnyxDocument[];
|
||||
toggleDocumentSelection: () => void;
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [finalQuery, setFinalQuery] = useState(query);
|
||||
@@ -87,28 +94,64 @@ export function SearchSummary({
|
||||
}, [query, isEditing]);
|
||||
|
||||
const searchingForDisplay = (
|
||||
<div className={`flex p-1 rounded ${isOverflowed && "cursor-default"}`}>
|
||||
<FiSearch className="flex-none mr-2 my-auto" size={14} />
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div
|
||||
className={`${!finished && "loading-text"}
|
||||
!text-sm !line-clamp-1 !break-all px-0.5`}
|
||||
ref={searchingForRef}
|
||||
className={`flex items-center w-full rounded ${
|
||||
isOverflowed && "cursor-default"
|
||||
}`}
|
||||
>
|
||||
{finished ? "Searched" : "Searching"} for:{" "}
|
||||
<i>
|
||||
{index === 1
|
||||
? finalQuery.length > 50
|
||||
? `${finalQuery.slice(0, 50)}...`
|
||||
: finalQuery
|
||||
: finalQuery}
|
||||
</i>
|
||||
<FiSearch className="mobile:hidden flex-none mr-2" size={14} />
|
||||
<div
|
||||
className={`${
|
||||
!finished && "loading-text"
|
||||
} text-xs desktop:text-sm mobile:ml-auto !line-clamp-1 !break-all px-0.5 flex-grow`}
|
||||
ref={searchingForRef}
|
||||
>
|
||||
{finished ? "Searched" : "Searching"} for:{" "}
|
||||
<i>
|
||||
{index === 1
|
||||
? finalQuery.length > 50
|
||||
? `${finalQuery.slice(0, 50)}...`
|
||||
: finalQuery
|
||||
: finalQuery}
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="desktop:hidden">
|
||||
{" "}
|
||||
{docs && (
|
||||
<button
|
||||
className="cursor-pointer mr-2 flex items-center gap-0.5"
|
||||
onClick={() => toggleDocumentSelection()}
|
||||
>
|
||||
{Array.from(new Set(docs.map((doc) => doc.source_type)))
|
||||
.slice(0, 3)
|
||||
.map((sourceType, idx) => (
|
||||
<div key={idx} className="rounded-full">
|
||||
<SourceIcon sourceType={sourceType} iconSize={14} />
|
||||
</div>
|
||||
))}
|
||||
{Array.from(new Set(docs.map((doc) => doc.source_type))).length >
|
||||
3 && (
|
||||
<div className="rounded-full bg-gray-200 w-3.5 h-3.5 flex items-center justify-center">
|
||||
<span className="text-[8px]">
|
||||
+
|
||||
{Array.from(new Set(docs.map((doc) => doc.source_type)))
|
||||
.length - 3}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs underline">View sources</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const editInput = handleSearchQueryEdit ? (
|
||||
<div className="flex w-full mr-3">
|
||||
<div className="my-2 w-full">
|
||||
<div className="mobile:hidden flex w-full mr-3">
|
||||
<div className="w-full">
|
||||
<input
|
||||
ref={editQueryRef}
|
||||
value={finalQuery}
|
||||
@@ -128,10 +171,10 @@ export function SearchSummary({
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
className="px-1 py-0.5 h-[28px] text-sm mr-2 w-full rounded-sm border border-border-strong"
|
||||
className="px-1 py-0.5 h-[22px] text-sm mr-2 w-full rounded-sm border border-border-strong"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 my-auto flex">
|
||||
<div className="ml-2 -my-1 my-auto flex">
|
||||
<Hoverable
|
||||
icon={FiCheck}
|
||||
onClick={() => {
|
||||
@@ -155,12 +198,12 @@ export function SearchSummary({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="flex items-center">
|
||||
{isEditing ? (
|
||||
editInput
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm">
|
||||
<div className="mobile:w-full mobile:mr-2 text-sm mobile:flex-grow">
|
||||
{isOverflowed ? (
|
||||
<HoverPopup
|
||||
mainContent={searchingForDisplay}
|
||||
@@ -176,12 +219,13 @@ export function SearchSummary({
|
||||
searchingForDisplay
|
||||
)}
|
||||
</div>
|
||||
|
||||
{handleSearchQueryEdit && (
|
||||
<TooltipProvider delayDuration={1000}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="my-auto hover:bg-hover p-1.5 rounded"
|
||||
className="ml-2 -my-2 mobile:hidden hover:bg-hover p-1 rounded flex-shrink-0"
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EmphasizedClickable } from "@/components/BasicClickable";
|
||||
import { BasicClickable } from "@/components/BasicClickable";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { FiBook } from "react-icons/fi";
|
||||
|
||||
export function SkippedSearch({
|
||||
@@ -7,22 +8,37 @@ export function SkippedSearch({
|
||||
handleForceSearch: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex text-sm !pt-0 p-1">
|
||||
<div className="flex mb-auto">
|
||||
<FiBook className="my-auto flex-none mr-2" size={14} />
|
||||
<div className="my-auto cursor-default">
|
||||
<div className="flex w-full text-sm !pt-0 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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
384
web/src/app/chat/nrf/NRFPage.tsx
Normal file
384
web/src/app/chat/nrf/NRFPage.tsx
Normal 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'll see your browser'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>
|
||||
);
|
||||
}
|
||||
5
web/src/app/chat/nrf/interfaces.ts
Normal file
5
web/src/app/chat/nrf/interfaces.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Shortcut {
|
||||
name: string;
|
||||
url: string;
|
||||
favicon?: string;
|
||||
}
|
||||
20
web/src/app/chat/nrf/page.tsx
Normal file
20
web/src/app/chat/nrf/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
20
web/src/app/chat/sessionSidebar/ChatGroup.tsx
Normal file
20
web/src/app/chat/sessionSidebar/ChatGroup.tsx
Normal 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;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
179
web/src/app/components/nrf/SettingsPanel.tsx
Normal file
179
web/src/app/components/nrf/SettingsPanel.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
46
web/src/app/components/nrf/ShortcutsDisplay.tsx
Normal file
46
web/src/app/components/nrf/ShortcutsDisplay.tsx
Normal 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
0
web/src/app/ee/Hori
Normal file
@@ -10,7 +10,7 @@ export default function WrappedAssistantsStats({
|
||||
assistantId: number;
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper page="chat" initiallyToggled={initiallyToggled}>
|
||||
<SidebarWrapper initiallyToggled={initiallyToggled}>
|
||||
<AssistantStats assistantId={assistantId} />
|
||||
</SidebarWrapper>
|
||||
);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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" : ""
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
123
web/src/components/context/NRFPreferencesContext.tsx
Normal file
123
web/src/components/context/NRFPreferencesContext.tsx
Normal 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;
|
||||
}
|
||||
257
web/src/components/extension/Shortcuts.tsx
Normal file
257
web/src/components/extension/Shortcuts.tsx
Normal 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'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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
_CompletedWelcomeFlowDummyComponent,
|
||||
_WelcomeModal,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user