Compare commits

...

68 Commits

Author SHA1 Message Date
Raunak Bhagat
91eaf23162 Refactor ChatPage 2025-09-22 17:51:07 -07:00
Raunak Bhagat
bdafbfe0e8 Edit AdminSidebar width 2025-09-22 11:22:25 -07:00
Raunak Bhagat
278fd0e153 Fix error in which message would not be updated after edit 2025-09-22 11:04:15 -07:00
Raunak Bhagat
a4bb97bc22 Fix editing modal 2025-09-22 10:53:55 -07:00
Raunak Bhagat
8063d9a75e Edit KG configuration page 2025-09-22 05:04:42 -07:00
Raunak Bhagat
1ffaba12f0 Fix height of search bar 2025-09-22 04:45:41 -07:00
Raunak Bhagat
26f8660663 Fix search bar 2025-09-22 04:40:12 -07:00
Raunak Bhagat
d6504ed578 Update search-settings page 2025-09-22 03:59:35 -07:00
Raunak Bhagat
7fcc2c9d35 Clean up more admin stuff 2025-09-22 03:21:48 -07:00
Raunak Bhagat
46e8f925fe Clean up AdminSidebar 2025-09-21 22:33:26 -07:00
Raunak Bhagat
5ec1f61839 Add user settings 2025-09-19 20:07:11 -07:00
Raunak Bhagat
df950963a7 Edit SvgMoreHorizontal SVG size 2025-09-19 19:47:06 -07:00
Raunak Bhagat
93208a66ac Edit settings popup state transitions 2025-09-19 19:30:39 -07:00
Raunak Bhagat
a4819e07e7 Small bug fixes 2025-09-19 19:19:37 -07:00
Raunak Bhagat
f642ace40c Implement logout 2025-09-19 19:16:24 -07:00
Raunak Bhagat
9b430ae2d5 Implement notifications 2025-09-19 19:03:48 -07:00
Raunak Bhagat
05f3f878b2 Edit edit/delete modals 2025-09-19 17:54:24 -07:00
Raunak Bhagat
df17c5352e Edit active colours 2025-09-19 16:46:08 -07:00
Raunak Bhagat
bcfb0f3cf3 Remove commented out state 2025-09-19 16:04:13 -07:00
Raunak Bhagat
38468c1dc4 Fix AgentsModal 2025-09-19 15:55:40 -07:00
Raunak Bhagat
8550a9c5e3 Cleanup sidebar a bit more 2025-09-19 14:33:36 -07:00
Raunak Bhagat
fe0c60e50d Fix UX around naming chats 2025-09-19 14:03:02 -07:00
Raunak Bhagat
4ecc151a02 Fix up chat-renaming 2025-09-19 13:49:08 -07:00
Raunak Bhagat
d08becead5 Saving changes 2025-09-19 13:34:54 -07:00
Raunak Bhagat
a429f852d5 Reduce height of buttons 2025-09-19 08:05:50 -07:00
Raunak Bhagat
a856f27fae Saving changes 2025-09-18 20:19:25 -07:00
Raunak Bhagat
d0d8027928 Edit popups in sidebar buttons 2025-09-18 19:53:33 -07:00
Raunak Bhagat
bd1671f1a1 Edit popovers and add new icons 2025-09-18 19:05:16 -07:00
Raunak Bhagat
e236c67678 Fix build errors 2025-09-18 17:28:19 -07:00
Raunak Bhagat
683956697a More UI fixes and tweaks 2025-09-18 16:57:47 -07:00
Raunak Bhagat
fb1e303ffc Fix ordering bug 2025-09-18 16:09:25 -07:00
Raunak Bhagat
729d4fafd1 Remove client directive 2025-09-18 15:52:16 -07:00
Raunak Bhagat
40c60282d0 Update agents modal and general structure of app 2025-09-18 15:19:35 -07:00
Raunak Bhagat
2141fd2c6e More edits to styling + colours 2025-09-18 12:45:50 -07:00
Raunak Bhagat
9aeba96043 Update state management 2025-09-16 19:34:48 -07:00
Raunak Bhagat
b431de5141 Update hover state for buttons 2025-09-16 19:06:05 -07:00
Raunak Bhagat
d1a6340cfc Add new chat handler 2025-09-16 17:45:21 -07:00
Raunak Bhagat
ccf382ef4f Edit spacing 2025-09-16 17:41:56 -07:00
Raunak Bhagat
c31997b9b2 Save folded state to localStorage 2025-09-16 17:40:11 -07:00
Raunak Bhagat
ab31795a46 Recenter icon when title is hidden 2025-09-16 17:35:40 -07:00
Raunak Bhagat
b3beca63dc Make headers sticky 2025-09-16 17:33:21 -07:00
Raunak Bhagat
cc6d54c1e6 Add loading state for Truncated component + fix spacings 2025-09-16 17:28:54 -07:00
Raunak Bhagat
ee12c0c5de Fix scrolling issue 2025-09-16 17:00:40 -07:00
Raunak Bhagat
d48912a05d Fix errors 2025-09-16 15:50:48 -07:00
Raunak Bhagat
c079072676 Remove unnecessary file + make HistorySidebar be smart 2025-09-16 15:48:15 -07:00
Raunak Bhagat
952f6bfb37 Delete unused files 2025-09-16 15:42:36 -07:00
Raunak Bhagat
0714e4bb4e Fix dnd 2025-09-16 15:39:47 -07:00
Raunak Bhagat
ae577f0f44 Add AgentsModal 2025-09-16 15:35:59 -07:00
Raunak Bhagat
0705d584d8 Update user hover-card 2025-09-16 14:52:54 -07:00
Raunak Bhagat
36e391e557 Add folded sidebar (+ shortcuts) 2025-09-16 13:50:49 -07:00
Raunak Bhagat
1efce594b5 Clean up truncation + buttons 2025-09-16 13:02:16 -07:00
Raunak Bhagat
67ac53f17d Add more styling for HistorySidebar + add README for working w/ icons 2025-09-16 11:21:13 -07:00
Raunak Bhagat
d5a222925a Add icons (as raw TSX) 2025-09-16 09:51:25 -07:00
Raunak Bhagat
d5ef928782 Add icons 2025-09-16 09:12:12 -07:00
Raunak Bhagat
6963d78f8e Fix more build errors? 2025-09-15 17:39:24 -07:00
Raunak Bhagat
d3ef2b8c17 Fix build errors 2025-09-15 17:28:34 -07:00
Raunak Bhagat
70f4162ea8 Update name 2025-09-15 17:17:09 -07:00
Raunak Bhagat
883f52d332 Update component names 2025-09-15 17:09:13 -07:00
Raunak Bhagat
f8fd83c883 Clean up sidebar 2025-09-15 15:56:45 -07:00
Raunak Bhagat
d2bf0c0c5f Update token-context bar 2025-09-15 11:43:54 -07:00
Raunak Bhagat
5d598c2d22 Add more colour fixes to Modal 2025-09-15 11:13:45 -07:00
Raunak Bhagat
9dc0e97302 Merge branch 'main' into colours 2025-09-15 09:45:32 -07:00
Raunak Bhagat
048b2a6b39 Edit LLMPopover and add border-radii 2025-09-15 09:43:06 -07:00
Raunak Bhagat
7dd3cecf67 Edit UserDropdown colours 2025-09-15 09:15:56 -07:00
Raunak Bhagat
82abe28986 Update more colours 2025-09-14 20:37:33 -07:00
Raunak Bhagat
a0575e6a00 Update colours for sidebar 2025-09-14 20:24:25 -07:00
Raunak Bhagat
0c5bf5b3ed Add all colours from Figma 2025-09-11 13:34:54 -07:00
Raunak Bhagat
492117d910 Edit .gitignore 2025-09-11 12:32:53 -07:00
143 changed files with 6966 additions and 7795 deletions

87
web/package-lock.json generated
View File

@@ -16,10 +16,12 @@
"@headlessui/tailwindcss": "^0.2.1",
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-radio-group": "^1.2.2",
@@ -3386,6 +3388,33 @@
}
}
},
"node_modules/@radix-ui/react-avatar": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-is-hydrated": "0.1.0",
"@radix-ui/react-use-layout-effect": "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-checkbox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
@@ -3638,6 +3667,37 @@
}
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"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-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -4211,6 +4271,24 @@
}
}
},
"node_modules/@radix-ui/react-use-is-hydrated": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.5.0"
},
"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-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
@@ -7964,6 +8042,7 @@
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -10961,6 +11040,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"engines": {
"node": ">=0.8.19"
}
@@ -19187,7 +19267,8 @@
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
@@ -20081,7 +20162,8 @@
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/thenify": {
"version": "3.3.1",
@@ -21341,6 +21423,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.7"

View File

@@ -22,10 +22,12 @@
"@headlessui/tailwindcss": "^0.2.1",
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-radio-group": "^1.2.2",

View File

@@ -1,4 +1,5 @@
"use client";
import { AdminPageTitle } from "@/components/admin/Title";
import { ConnectorIcon } from "@/components/icons/icons";
import { SourceCategory, SourceMetadata } from "@/lib/search/interfaces";
@@ -34,17 +35,23 @@ import { Credential } from "@/lib/connectors/credentials";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import SourceTile from "@/components/SourceTile";
interface CategorizedSources {
[key: string]: SourceMetadata[];
}
interface SourceTileTooltipWrapperProps {
sourceMetadata: SourceMetadata;
preSelect?: boolean;
federatedConnectors?: FederatedConnectorDetail[];
slackCredentials?: Credential<any>[];
}
function SourceTileTooltipWrapper({
sourceMetadata,
preSelect,
federatedConnectors,
slackCredentials,
}: {
sourceMetadata: SourceMetadata;
preSelect?: boolean;
federatedConnectors?: FederatedConnectorDetail[];
slackCredentials?: Credential<any>[];
}) {
}: SourceTileTooltipWrapperProps) {
// Check if there's already a federated connector for this source
const existingFederatedConnector = useMemo(() => {
if (!sourceMetadata.federated || !federatedConnectors) {
@@ -189,7 +196,7 @@ export default function Page() {
);
}, [sources, filterSources, searchTerm]);
const categorizedSources = useMemo(() => {
const categorizedSources = useMemo((): CategorizedSources => {
const filtered = filterSources(sources);
const categories = Object.values(SourceCategory).reduce(
(acc, category) => {
@@ -286,7 +293,7 @@ export default function Page() {
value={rawSearchTerm} // keep the input bound to immediate state
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-01 bg-background-tint-03 px-3 py-1 text-sm shadow-01 transition-colors placeholder:text-text-03 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-theme-primary-04"
/>
{dedupedPopular.length > 0 && (

View File

@@ -96,6 +96,7 @@ import TextView from "@/components/chat/TextView";
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
import { MAX_CHARACTERS_PERSONA_DESCRIPTION } from "@/lib/constants";
import { FormErrorFocus } from "@/components/FormErrorHelpers";
import { useAgentsContext } from "@/components-2/context/AgentsContext";
function findSearchTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === SEARCH_TOOL_ID);
@@ -122,6 +123,17 @@ function SubLabel({ children }: { children: string | JSX.Element }) {
);
}
interface AssistantEditorProps {
existingPersona?: FullPersona | null;
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSetSummary[];
user: User | null;
defaultPublic: boolean;
llmProviders: LLMProviderView[];
tools: ToolSnapshot[];
shouldAddAssistantToUserPreferences?: boolean;
}
export function AssistantEditor({
existingPersona,
ccPairs,
@@ -131,17 +143,9 @@ export function AssistantEditor({
llmProviders,
tools,
shouldAddAssistantToUserPreferences,
}: {
existingPersona?: FullPersona | null;
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSetSummary[];
user: User | null;
defaultPublic: boolean;
llmProviders: LLMProviderView[];
tools: ToolSnapshot[];
shouldAddAssistantToUserPreferences?: boolean;
}) {
const { refreshAssistants } = useAssistantsContext();
}: AssistantEditorProps) {
// const { refreshAssistants } = useAssistantsContext();
const { refreshAgents } = useAgentsContext();
const router = useRouter();
const searchParams = useSearchParams();
@@ -452,7 +456,7 @@ export function AssistantEditor({
if (existingPersona) {
const response = await deletePersona(existingPersona.id);
if (response.ok) {
await refreshAssistants();
await refreshAgents();
router.push(
isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat`
);
@@ -703,7 +707,7 @@ export function AssistantEditor({
message: `"${assistant.name}" has been added to your list.`,
type: "success",
});
await refreshAssistants();
await refreshAgents();
} else {
setPopup({
message: `"${assistant.name}" could not be added to your list.`,
@@ -712,7 +716,7 @@ export function AssistantEditor({
}
}
await refreshAssistants();
await refreshAgents();
await refreshFolders();
router.push(
@@ -787,7 +791,7 @@ export function AssistantEditor({
Edit assistant <b>{existingPersona.name}</b>
</>
) : (
"Create an Assistant"
"Create an Agent"
)}
</p>
<div className="max-w-4xl w-full">

View File

@@ -8,6 +8,7 @@ import useSWR from "swr";
import { ThreeDotsLoader } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { Lock } from "@phosphor-icons/react";
import Text from "@/components-2/Text";
function Main() {
const {
@@ -53,71 +54,72 @@ function Main() {
return <ThreeDotsLoader />;
}
return (
<div className="container mx-auto p-4">
<CardSection className="mb-8 max-w-2xl bg-white text-text shadow-lg rounded-lg">
<h3 className="text-2xl text-text-800 font-bold mb-4 text-text border-b border-b-border pb-2">
Process with Unstructured API
</h3>
<CardSection className="max-w-2xl flex flex-col gap-spacing-paragraph">
<Text headingH2>Process with Unstructured API</Text>
<div className="space-y-4">
<p className="text-text-600">
Unstructured extracts and transforms complex data from formats like
.pdf, .docx, .png, .pptx, etc. into clean text for Onyx to ingest.
Provide an API key to enable Unstructured document processing.
<br />
<br /> <strong>Note:</strong> this will send documents to
Unstructured servers for processing.
</p>
<p className="text-text-600">
Learn more about Unstructured{" "}
<a
href="https://docs.unstructured.io/welcome"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline font-medium"
>
here
</a>
.
</p>
<div className="mt-4">
{isApiKeySet ? (
<div className="w-full p-3 border rounded-md bg-background text-text flex items-center">
<span className="flex-grow"></span>
<Lock className="h-5 w-5 text-text-400" />
</div>
) : (
<input
type="text"
placeholder="Enter API Key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="w-full p-3 border rounded-md bg-background text-text focus:ring-2 focus:ring-blue-500 transition duration-200"
/>
)}
</div>
<div className="flex space-x-4 mt-6">
{isApiKeySet ? (
<>
<Button onClick={handleDelete} variant="destructive">
Delete API Key
</Button>
<p className="text-text-600 my-auto">
Delete the current API key before updating.
</p>
</>
) : (
<Button
onClick={handleSave}
className="bg-blue-500 text-white hover:bg-blue-600 transition duration-200"
>
Save API Key
</Button>
)}
</div>
<div className="border-b" />
<div className="space-y-4">
<Text text04>
Unstructured extracts and transforms complex data from formats like
.pdf, .docx, .png, .pptx, etc. into clean text for Onyx to ingest.
Provide an API key to enable Unstructured document processing.
<br />
<br />
<strong>Note:</strong> This will send documents to Unstructured
servers for processing.
</Text>
<Text text04>
Learn more about Unstructured{" "}
<a
href="https://docs.unstructured.io/welcome"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline font-medium"
>
here
</a>
.
</Text>
<div className="mt-4">
{isApiKeySet ? (
<div className="w-full p-3 border rounded-md bg-background text-text flex items-center">
<span className="flex-grow"></span>
<Lock className="h-5 w-5 text-text-400" />
</div>
) : (
<input
type="text"
placeholder="Enter API Key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="w-full p-3 border rounded-md bg-background text-text focus:ring-2 focus:ring-blue-500 transition duration-200"
/>
)}
</div>
</CardSection>
</div>
<div className="flex space-x-4 mt-6">
{isApiKeySet ? (
<>
<Button onClick={handleDelete} variant="destructive">
Delete API Key
</Button>
<p className="text-text-600 my-auto">
Delete the current API key before updating.
</p>
</>
) : (
<Button
onClick={handleSave}
className="bg-blue-500 text-white hover:bg-blue-600 transition duration-200"
>
Save API Key
</Button>
)}
</div>
</div>
</CardSection>
);
}

View File

@@ -3,11 +3,11 @@
import { ThreeDotsLoader } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { errorHandlingFetcher } from "@/lib/fetcher";
import Text from "@/components/ui/text";
import Text from "@/components-2/Text";
import Title from "@/components/ui/title";
import { Button } from "@/components/ui/button";
import useSWR from "swr";
import { ModelPreview } from "../../../../components/embedding/ModelSelector";
import { ModelPreview } from "@/components/embedding/ModelSelector";
import {
HostedEmbeddingModel,
CloudEmbeddingModel,
@@ -84,104 +84,102 @@ function Main() {
}
return (
<div className="h-screen">
<div className="py-padding-content">
{searchSettingsPopup}
{!futureEmbeddingModel ? (
<>
{futureEmbeddingModel ? (
<UpgradingPage futureEmbeddingModel={futureEmbeddingModel} />
) : (
<div className="flex flex-col gap-padding-content">
{settings?.settings.needs_reindexing && (
<p className="max-w-3xl">
<Text>
Your search settings are currently out of date! We recommend
updating your search settings and re-indexing.
</p>
)}
<Title className="mb-6 mt-8 !text-2xl">Embedding Model</Title>
{currentEmeddingModel ? (
<ModelPreview model={currentEmeddingModel} display showDetails />
) : (
<Title className="mt-8 mb-4">Choose your Embedding Model</Title>
</Text>
)}
<Title className="mb-2 mt-8 !text-2xl">Post-processing</Title>
<div className="flex flex-col gap-padding-button">
<Text headingH2>Embedding Model</Text>
{currentEmeddingModel ? (
<ModelPreview model={currentEmeddingModel} />
) : (
<Text>Choose your Embedding Model</Text>
)}
</div>
<CardSection className="!mr-auto mt-8 !w-96">
{searchSettings && (
<>
<div className="px-1 w-full rounded-lg">
<div className="space-y-4">
<div>
<Text className="font-semibold">Reranking Model</Text>
<Text className="text-text-700">
{searchSettings.rerank_model_name || "Not set"}
</Text>
</div>
<div className="flex flex-col gap-padding-button">
<Text headingH2>Post-processing</Text>
<CardSection className="!w-96">
{searchSettings && (
<>
<div className="px-1 w-full rounded-lg">
<div className="space-y-4">
<div>
<Text>Reranking Model</Text>
<Text text04>
{searchSettings.rerank_model_name || "Not set"}
</Text>
</div>
<div>
<Text className="font-semibold">Results to Rerank</Text>
<Text className="text-text-700">
{searchSettings.num_rerank}
</Text>
</div>
<div>
<Text>Results to Rerank</Text>
<Text text04>{searchSettings.num_rerank}</Text>
</div>
<div>
<Text className="font-semibold">
Multilingual Expansion
</Text>
<Text className="text-text-700">
{searchSettings.multilingual_expansion.length > 0
? searchSettings.multilingual_expansion.join(", ")
: "None"}
</Text>
</div>
<div>
<Text>Multilingual Expansion</Text>
<Text text04>
{searchSettings.multilingual_expansion.length > 0
? searchSettings.multilingual_expansion.join(", ")
: "None"}
</Text>
</div>
<div>
<Text className="font-semibold">Multipass Indexing</Text>
<Text className="text-text-700">
{searchSettings.multipass_indexing
? "Enabled"
: "Disabled"}
</Text>
</div>
<div>
<Text>Multipass Indexing</Text>
<Text text04>
{searchSettings.multipass_indexing
? "Enabled"
: "Disabled"}
</Text>
</div>
<div>
<Text className="font-semibold">Contextual RAG</Text>
<Text className="text-text-700">
{searchSettings.enable_contextual_rag
? "Enabled"
: "Disabled"}
</Text>
</div>
<div>
<Text>Contextual RAG</Text>
<Text text04>
{searchSettings.enable_contextual_rag
? "Enabled"
: "Disabled"}
</Text>
</div>
<div>
<Text className="font-semibold">
Disable Reranking for Streaming
</Text>
<Text className="text-text-700">
{searchSettings.disable_rerank_for_streaming
? "Yes"
: "No"}
</Text>
<div>
<Text>Disable Reranking for Streaming</Text>
<Text text04>
{searchSettings.disable_rerank_for_streaming
? "Yes"
: "No"}
</Text>
</div>
</div>
</div>
</div>
</>
)}
</CardSection>
</>
)}
</CardSection>
</div>
<Link href="/admin/embeddings">
<Button variant="navigate" className="mt-8">
<Button variant="navigate" className="mt-spacing-paragraph">
Update Search Settings
</Button>
</Link>
</>
) : (
<UpgradingPage futureEmbeddingModel={futureEmbeddingModel} />
</div>
)}
</div>
);
}
function Page() {
export default function Page() {
return (
<div className="mx-auto container">
<AdminPageTitle
@@ -192,5 +190,3 @@ function Page() {
</div>
);
}
export default Page;

View File

@@ -2,7 +2,7 @@ import { SourceIcon } from "@/components/SourceIcon";
import React, { useEffect, useState } from "react";
import { Switch } from "@/components/ui/switch";
import Link from "next/link";
import { EntityType, SourceAndEntityTypeView } from "./interfaces";
import { EntityType, SourceAndEntityTypeView } from "@/app/admin/kg/interfaces";
import CollapsibleCard from "@/components/CollapsibleCard";
import { ValidSources } from "@/lib/types";
import { FaCircleQuestion } from "react-icons/fa6";
@@ -29,7 +29,7 @@ function snakeToHumanReadable(str: string): string {
// Custom Header Component
function TableHeader() {
return (
<div className="grid grid-cols-12 gap-y-4 px-8 p-4 border-b border-neutral-700 font-semibold text-sm bg-neutral-900 text-neutral-500">
<div className="grid grid-cols-12 gap-y-4 px-8 p-4 border-b border-border font-semibold text-sm bg-background-50 text-text-600">
<div className="col-span-1">Entity Name</div>
<div className="col-span-10">Description</div>
<div className="col-span-1 flex flex-1 justify-center">Active</div>
@@ -38,7 +38,11 @@ function TableHeader() {
}
// Custom Row Component
function TableRow({ entityType }: { entityType: EntityType }) {
interface TableRowProps {
entityType: EntityType;
}
function TableRow({ entityType }: TableRowProps) {
const [entityTypeState, setEntityTypeState] = useState(entityType);
const [descriptionSavingState, setDescriptionSavingState] = useState<
"saving" | "saved" | "failed" | undefined
@@ -153,7 +157,7 @@ function TableRow({ entityType }: { entityType: EntityType }) {
}`}
style={{ zIndex: 1 }}
>
<span className="inline-block w-4 h-4 align-middle border-2 border-blue-400 border-t-transparent rounded-full animate-spin" />
<span className="inline-block w-4 h-4 align-middle border-2 border-status-info-05 border-t-transparent rounded-full animate-spin" />
</span>
<span
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-400 ease-in-out ${
@@ -161,7 +165,7 @@ function TableRow({ entityType }: { entityType: EntityType }) {
}`}
style={{ zIndex: 2 }}
>
<CheckmarkIcon size={16} className="text-green-400" />
<CheckmarkIcon size={16} className="text-status-success-05" />
</span>
</span>
</div>
@@ -278,18 +282,18 @@ export default function KGEntityTypes({
{snakeToHumanReadable(key)}
<span className="ml-auto flex flex-row gap-x-16 items-center pr-16">
<span className="flex flex-col items-end">
<span className="text-sm text-neutral-400 mb-0.5">
<span className="text-sm text-text-600 mb-0.5">
Entities Count
</span>
<span className="text-xl text-neutral-100 font-semibold flex w-full">
<span className="text-xl text-text font-semibold flex w-full">
{stats.entities_count}
</span>
</span>
<span className="flex flex-col items-end">
<span className="text-sm text-neutral-400 mb-0.5">
<span className="text-sm text-text-600 mb-0.5">
Last Updated
</span>
<span className="text-xl text-neutral-100 font-semibold flex w-full">
<span className="text-xl text-text font-semibold flex w-full">
{stats.last_updated
? new Date(stats.last_updated).toLocaleString()
: "N/A"}

View File

@@ -10,21 +10,25 @@ import {
} from "@/components/Field";
import { BrainIcon } from "@/components/icons/icons";
import { Modal } from "@/components/Modal";
import { Button } from "@/components/ui/button";
import { Button as UiButton } from "@/components/ui/button";
import { SwitchField } from "@/components/ui/switch";
import { Form, Formik, FormikState, useFormikContext } from "formik";
import { useState } from "react";
import { FiSettings } from "react-icons/fi";
import * as Yup from "yup";
import { KGConfig, KGConfigRaw, SourceAndEntityTypeView } from "./interfaces";
import { sanitizeKGConfig } from "./utils";
import {
KGConfig,
KGConfigRaw,
SourceAndEntityTypeView,
} from "@/app/admin/kg/interfaces";
import { sanitizeKGConfig } from "@/app/admin/kg/utils";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import Title from "@/components/ui/title";
import { redirect } from "next/navigation";
import { useIsKGExposed } from "./utils";
import KGEntityTypes from "./KGEntityTypes";
import { useIsKGExposed } from "@/app/admin/kg/utils";
import KGEntityTypes from "@/app/admin/kg/KGEntityTypes";
import Button from "@/components-2/buttons/Button";
function createDomainField(
name: string,
@@ -33,7 +37,11 @@ function createDomainField(
placeholder: string,
minFields?: number
) {
return function DomainFields({ disabled = false }: { disabled?: boolean }) {
interface DomainFieldsProps {
disabled?: boolean;
}
return function DomainFields({ disabled = false }: DomainFieldsProps) {
const { values } = useFormikContext<any>();
return (
@@ -65,17 +73,19 @@ const IgnoreDomains = createDomainField(
"Domain"
);
interface KGConfigurationProps {
kgConfig: KGConfig;
onSubmitSuccess?: () => void;
setPopup?: (spec: PopupSpec | null) => void;
entityTypesMutate?: () => void;
}
function KGConfiguration({
kgConfig,
onSubmitSuccess,
setPopup,
entityTypesMutate,
}: {
kgConfig: KGConfig;
onSubmitSuccess?: () => void;
setPopup?: (spec: PopupSpec | null) => void;
entityTypesMutate?: () => void;
}) {
}: KGConfigurationProps) {
const initialValues: KGConfig = {
enabled: kgConfig.enabled,
vendor: kgConfig.vendor ?? "",
@@ -200,9 +210,9 @@ function KGConfiguration({
disabled={!props.values.enabled}
/>
</div>
<Button variant="submit" type="submit" disabled={!props.dirty}>
<UiButton variant="submit" type="submit" disabled={!props.dirty}>
Submit
</Button>
</UiButton>
</div>
</Form>
)}
@@ -274,11 +284,8 @@ function Main() {
Begin by configuring some high-level attributes, and then define the
entities you want to model afterwards.
</p>
<Button
size="lg"
icon={FiSettings}
onClick={() => setConfigureModalShown(true)}
>
<Button onClick={() => setConfigureModalShown(true)}>
Configure Knowledge Graph
</Button>
</div>

View File

@@ -1,9 +1,9 @@
import { Layout } from "@/components/admin/Layout";
import { Layout as GenericLayout } from "@/components/admin/Layout";
export default async function AdminLayout({
children,
}: {
interface LayoutProps {
children: React.ReactNode;
}) {
return await Layout({ children });
}
export default async function Layout({ children }: LayoutProps) {
return await GenericLayout({ children });
}

View File

@@ -1,19 +1,19 @@
"use client";
import { useState } from "react";
import { FiPlusSquare } from "react-icons/fi";
import useSWR, { mutate } from "swr";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import InvitedUserTable from "@/components/admin/users/InvitedUserTable";
import SignedUpUserTable from "@/components/admin/users/SignedUpUserTable";
import { FiPlusSquare } from "react-icons/fi";
import { Modal } from "@/components/Modal";
import { ThreeDotsLoader } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
import { UsersIcon } from "@/components/icons/icons";
import { errorHandlingFetcher } from "@/lib/fetcher";
import useSWR, { mutate } from "swr";
import { ErrorCallout } from "@/components/ErrorCallout";
import BulkAdd from "@/components/admin/users/BulkAdd";
import Text from "@/components/ui/text";
@@ -22,13 +22,13 @@ import { SearchBar } from "@/components/search/SearchBar";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
import PendingUsersTable from "@/components/admin/users/PendingUsersTable";
const UsersTables = ({
q,
setPopup,
}: {
interface UsersTablesProps {
q: string;
setPopup: (spec: PopupSpec) => void;
}) => {
}
function UsersTables({ q, setPopup }: UsersTablesProps) {
const {
data: invitedUsers,
error: invitedUsersError,
@@ -130,9 +130,9 @@ const UsersTables = ({
)}
</Tabs>
);
};
}
const SearchableTables = () => {
function SearchableTables() {
const { popup, setPopup } = usePopup();
const [query, setQuery] = useState("");
const [q, setQ] = useState("");
@@ -155,13 +155,13 @@ const SearchableTables = () => {
</div>
</div>
);
};
}
const AddUserButton = ({
setPopup,
}: {
interface AddUserButtonProps {
setPopup: (spec: PopupSpec) => void;
}) => {
}
function AddUserButton({ setPopup }: AddUserButtonProps) {
const [modal, setModal] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
@@ -241,15 +241,13 @@ const AddUserButton = ({
)}
</>
);
};
}
const Page = () => {
export default function Page() {
return (
<div className="mx-auto container">
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
<SearchableTables />
</div>
);
};
export default Page;
}

View File

@@ -1,159 +0,0 @@
"use client";
import Cookies from "js-cookie";
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
import { ReactNode, useCallback, useContext, useRef, useState } from "react";
import { useSidebarVisibility } from "@/components/chat/hooks";
import FunctionalHeader from "@/components/chat/Header";
import { useRouter } from "next/navigation";
import FixedLogo from "../../components/logo/FixedLogo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useChatContext } from "@/components/context/ChatContext";
import { HistorySidebar } from "@/components/sidebar/HistorySidebar";
import AssistantModal from "./mine/AssistantModal";
import { useSidebarShortcut } from "@/lib/browserUtilities";
import { UserSettingsModal } from "@/app/chat/components/modal/UserSettingsModal";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useUser } from "@/components/user/UserProvider";
import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus";
interface SidebarWrapperProps<T extends object> {
size?: "sm" | "lg";
children: ReactNode;
}
export default function SidebarWrapper<T extends object>({
size = "sm",
children,
}: SidebarWrapperProps<T>) {
const { sidebarInitiallyVisible: initiallyToggled } = useChatContext();
const [sidebarVisible, setSidebarVisible] = 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 toggleSidebar = useCallback(() => {
Cookies.set(
SIDEBAR_TOGGLED_COOKIE_NAME,
String(!sidebarVisible).toLocaleLowerCase(),
{ path: "/" }
);
setSidebarVisible((sidebarVisible) => !sidebarVisible);
}, [sidebarVisible]);
const sidebarElementRef = useRef<HTMLDivElement>(null);
const { folders, chatSessions, ccPairs } = useChatContext();
const {
connectors: federatedConnectors,
refetch: refetchFederatedConnectors,
} = useFederatedOAuthStatus();
const explicitlyUntoggle = () => {
setShowDocSidebar(false);
setUntoggled(true);
setTimeout(() => {
setUntoggled(false);
}, 200);
};
const { popup, setPopup } = usePopup();
const settings = useContext(SettingsContext);
useSidebarVisibility({
sidebarVisible,
sidebarElementRef,
showDocSidebar,
setShowDocSidebar,
mobile: settings?.isMobile,
});
const { user } = useUser();
const [showAssistantsModal, setShowAssistantsModal] = useState(false);
const router = useRouter();
const [userSettingsToggled, setUserSettingsToggled] = useState(false);
const { llmProviders } = useChatContext();
useSidebarShortcut(router, toggleSidebar);
return (
<div className="flex relative overflow-x-hidden overscroll-contain flex-col w-full h-screen">
{popup}
{showAssistantsModal && (
<AssistantModal hideModal={() => setShowAssistantsModal(false)} />
)}
<div
ref={sidebarElementRef}
className={`
flex-none
fixed
left-0
z-30
bg-background-100
h-screen
transition-all
bg-opacity-80
duration-300
ease-in-out
${
!untoggled && (showDocSidebar || sidebarVisible)
? "opacity-100 w-[250px] translate-x-0"
: "opacity-0 w-[200px] pointer-events-none -translate-x-10"
}`}
>
<div className="w-full relative">
{" "}
<HistorySidebar
setShowAssistantsModal={setShowAssistantsModal}
page={"chat"}
explicitlyUntoggle={explicitlyUntoggle}
ref={sidebarElementRef}
toggleSidebar={toggleSidebar}
toggled={sidebarVisible}
existingChats={chatSessions}
currentChatSession={null}
folders={folders}
/>
</div>
</div>
{userSettingsToggled && (
<UserSettingsModal
setPopup={setPopup}
llmProviders={llmProviders}
ccPairs={ccPairs}
federatedConnectors={federatedConnectors}
refetchFederatedConnectors={refetchFederatedConnectors}
onClose={() => setUserSettingsToggled(false)}
defaultModel={user?.preferences?.default_model!}
/>
)}
<div className="absolute px-2 left-0 w-full top-0">
<FunctionalHeader
removeHeight={true}
toggleUserSettings={() => setUserSettingsToggled(true)}
sidebarToggled={sidebarVisible}
toggleSidebar={toggleSidebar}
page="chat"
/>
<div className="w-full flex">
<div
style={{ transition: "width 0.30s ease-out" }}
className={`flex-none
overflow-y-hidden
bg-background-100
h-full
transition-all
bg-opacity-80
duration-300
ease-in-out
${sidebarVisible ? "w-[250px]" : "w-[0px]"}`}
/>
<div className={` w-full mx-auto`}>{children}</div>
</div>
</div>
<FixedLogo backgroundToggled={sidebarVisible || showDocSidebar} />
</div>
);
}

View File

@@ -1,371 +0,0 @@
import React, { useState, useRef, useLayoutEffect } from "react";
import { useRouter } from "next/navigation";
import {
FiMoreHorizontal,
FiTrash,
FiEdit,
FiBarChart,
FiLock,
FiUnlock,
} from "react-icons/fi";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { useUser } from "@/components/user/UserProvider";
import { useAssistantsContext } from "@/components/context/AssistantsContext";
import { checkUserOwnsAssistant } from "@/lib/assistants/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { PinnedIcon } from "@/components/icons/icons";
import { deletePersona } from "@/app/admin/assistants/lib";
import { PencilIcon } from "lucide-react";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { truncateString } from "@/lib/utils";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Button } from "@/components/ui/button";
export const AssistantBadge = ({
text,
className,
maxLength,
}: {
text: string;
className?: string;
maxLength?: number;
}) => {
return (
<div
className={`h-4 px-1.5 py-1 text-[10px] flex-none bg-neutral-200/50 dark:bg-neutral-700 rounded-lg justify-center items-center gap-1 inline-flex ${className}`}
>
<div className="text-text-800 font-normal leading-[8px]">
{maxLength ? truncateString(text, maxLength) : text}
</div>
</div>
);
};
const AssistantCard: React.FC<{
persona: MinimalPersonaSnapshot;
pinned: boolean;
closeModal: () => void;
}> = ({ persona, pinned, closeModal }) => {
const { user, toggleAssistantPinnedStatus } = useUser();
const router = useRouter();
const { refreshAssistants, pinnedAssistants } = useAssistantsContext();
const { popup, setPopup } = usePopup();
const isOwnedByUser = checkUserOwnsAssistant(user, persona);
const [activePopover, setActivePopover] = useState<string | null | undefined>(
undefined
);
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const [isDeleteConfirmation, setIsDeleteConfirmation] = useState(false);
const handleDelete = () => {
setIsDeleteConfirmation(true);
};
const confirmDelete = async () => {
const response = await deletePersona(persona.id);
if (response.ok) {
await refreshAssistants();
setActivePopover(null);
setIsDeleteConfirmation(false);
setPopup({
message: `${persona.name} has been successfully deleted.`,
type: "success",
});
} else {
setPopup({
message: `Failed to delete assistant - ${await response.text()}`,
type: "error",
});
}
};
const cancelDelete = () => {
setIsDeleteConfirmation(false);
};
const handleEdit = () => {
router.push(`/assistants/edit/${persona.id}`);
setActivePopover(null);
};
const closePopover = () => setActivePopover(undefined);
const nameRef = useRef<HTMLHeadingElement>(null);
const hiddenNameRef = useRef<HTMLSpanElement>(null);
const [isNameTruncated, setIsNameTruncated] = useState(false);
useLayoutEffect(() => {
const checkTruncation = () => {
if (nameRef.current && hiddenNameRef.current) {
const visibleWidth = nameRef.current.offsetWidth;
const fullTextWidth = hiddenNameRef.current.offsetWidth;
setIsNameTruncated(fullTextWidth > visibleWidth);
}
};
checkTruncation();
window.addEventListener("resize", checkTruncation);
return () => window.removeEventListener("resize", checkTruncation);
}, [persona.name]);
return (
<div className="w-full text-text-800 p-2 overflow-visible pb-4 pt-3 bg-transparent dark:bg-neutral-800/80 rounded shadow-[0px_0px_4px_0px_rgba(0,0,0,0.25)] flex flex-col">
{popup}
<div className="w-full flex">
<div className="ml-2 flex-none mr-2 mt-1 w-10 h-10">
<AssistantIcon assistant={persona} size="large" />
</div>
<div className="flex-1 mt-1 flex flex-col">
<div className="flex justify-between items-start mb-1">
<div className="flex items-end gap-x-2 leading-none">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<h3
ref={nameRef}
className={`text-neutral-900 dark:text-neutral-100 line-clamp-1 break-all text-ellipsis leading-none font-semibold text-base lg-normal w-full overflow-hidden`}
>
{persona.name}
</h3>
</TooltipTrigger>
{isNameTruncated && (
<TooltipContent>{persona.name}</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<span
ref={hiddenNameRef}
className="absolute left-0 top-0 invisible whitespace-nowrap"
aria-hidden="true"
>
{persona.name}
</span>
{persona.labels && persona.labels.length > 0 && (
<>
{persona.labels.slice(0, 2).map((label, index) => (
<AssistantBadge
key={index}
text={label.name}
maxLength={10}
/>
))}
{persona.labels.length > 2 && (
<AssistantBadge
text={`+${persona.labels.length - 2} more`}
/>
)}
</>
)}
</div>
{isOwnedByUser && (
<div className="flex ml-2 relative items-center gap-x-2">
<Popover>
<PopoverTrigger>
<button
type="button"
className="hover:bg-neutral-200 dark:hover:bg-neutral-700 p-1 -my-1 rounded-full"
aria-label="More Options"
>
<FiMoreHorizontal size={16} />
</button>
</PopoverTrigger>
<PopoverContent
className={`${
isDeleteConfirmation ? "w-64" : "w-32"
} z-[10000] p-2`}
>
{!isDeleteConfirmation ? (
<div className="flex flex-col text-sm space-y-1">
<button
onClick={isOwnedByUser ? handleEdit : undefined}
className={`w-full flex items-center text-left px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-700"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiEdit size={12} className="inline mr-2" />
Edit
</button>
{isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
<button
onClick={
isOwnedByUser
? () => {
router.push(
`/assistants/stats/${persona.id}`
);
closePopover();
}
: undefined
}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-800"
: "opacity-50 cursor-not-allowed"
}`}
>
<FiBarChart size={12} className="inline mr-2" />
Stats
</button>
)}
<button
onClick={isOwnedByUser ? handleDelete : undefined}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral- text-red-600 dark:text-red-400"
: "opacity-50 cursor-not-allowed text-red-300 dark:text-red-500"
}`}
disabled={!isOwnedByUser}
>
<FiTrash size={12} className="inline mr-2" />
Delete
</button>
</div>
) : (
<div className="w-full">
<p className="text-sm mb-3">
Are you sure you want to delete assistant{" "}
<b>{persona.name}</b>?
</p>
<div className="flex justify-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={cancelDelete}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
onClick={confirmDelete}
>
Delete
</Button>
</div>
</div>
)}
</PopoverContent>
</Popover>
</div>
)}
</div>
<p className="text-neutral-800 dark:text-neutral-200 font-[350] mt-0 text-sm line-clamp-2 h-[2.7em]">
{persona.description || "\u00A0"}
</p>
<div className="flex flex-col ">
<div className="my-1.5">
<p className="flex items-center text-neutral-800 dark:text-neutral-200 text-xs opacity-50">
{persona.owner?.email || persona.builtin_persona ? (
<>
<span className="truncate">
By {persona.owner?.email || "Onyx"}
</span>
<span className="mx-2"></span>
</>
) : null}
<span className="flex-none truncate">
{persona.tools.length > 0 ? (
<>
{persona.tools.length}
{" Action"}
{persona.tools.length !== 1 ? "s" : ""}
</>
) : (
"No Actions"
)}
</span>
<span className="mx-2"></span>
{persona.is_public ? (
<>
<FiUnlock size={12} className="inline mr-1" />
Public
</>
) : (
<>
<FiLock size={12} className="inline mr-1" />
Private
</>
)}
</p>
</div>
</div>
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => {
router.push(`/chat?assistantId=${persona.id}`);
closeModal();
}}
className="hover:bg-neutral-100 dark:hover:bg-neutral-700 dark:bg-[#2E2E2D] hover:text-neutral-900 dark:hover:text-neutral-100 px-2 py-1 gap-x-1 rounded border border-neutral-400 dark:border-neutral-600 flex items-center"
>
<PencilIcon 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>
<div
onClick={async () => {
await toggleAssistantPinnedStatus(
pinnedAssistants.map((a) => a.id),
persona.id,
!pinned
);
}}
className="hover:bg-neutral-100 dark:hover:bg-neutral-700 dark:bg-[#2E2E2D] px-2 group cursor-pointer py-1 gap-x-1 relative rounded border border-neutral-400 dark:border-neutral-600 flex items-center w-[65px]"
>
<PinnedIcon size={12} />
{!pinned ? (
<p className="absolute w-full left-0 group-hover:text-neutral-900 dark:group-hover:text-neutral-100 w-full text-center transform text-xs">
Pin
</p>
) : (
<p className="text-xs group-hover:text-neutral-900 dark:group-hover:text-neutral-100">
Unpin
</p>
)}
</div>
</TooltipTrigger>
<TooltipContent>
{pinned ? "Remove from" : "Add to"} your pinned list
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
<div className="flex items-center justify-center"></div>
</div>
);
};
export default AssistantCard;

View File

@@ -1,301 +0,0 @@
"use client";
import React, { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import AssistantCard from "./AssistantCard";
import { useAssistantsContext } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider";
import { FilterIcon, XIcon } from "lucide-react";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
export const AssistantBadgeSelector = ({
text,
selected,
toggleFilter,
}: {
text: string;
selected: boolean;
toggleFilter: () => void;
}) => {
return (
<div
className={`
select-none ${
selected
? "bg-background-900 text-white"
: "bg-transparent text-text-900"
} w-12 h-5 text-center px-1 py-0.5 rounded-lg cursor-pointer text-[12px] font-normal leading-[10px] border border-black justify-center items-center gap-1 inline-flex`}
onClick={toggleFilter}
>
{text}
</div>
);
};
export enum AssistantFilter {
Pinned = "Pinned",
Public = "Public",
Private = "Private",
Mine = "Mine",
}
const useAssistantFilter = () => {
const [assistantFilters, setAssistantFilters] = useState<
Record<AssistantFilter, boolean>
>({
[AssistantFilter.Pinned]: false,
[AssistantFilter.Public]: false,
[AssistantFilter.Private]: false,
[AssistantFilter.Mine]: false,
});
const toggleAssistantFilter = (filter: AssistantFilter) => {
setAssistantFilters((prevFilters) => ({
...prevFilters,
[filter]: !prevFilters[filter],
}));
};
return { assistantFilters, toggleAssistantFilter, setAssistantFilters };
};
interface AssistantModalProps {
hideModal: () => void;
}
export function AssistantModal({ hideModal }: AssistantModalProps) {
const { assistants, pinnedAssistants } = useAssistantsContext();
const { assistantFilters, toggleAssistantFilter } = useAssistantFilter();
const router = useRouter();
const { user } = useUser();
const [searchQuery, setSearchQuery] = useState("");
const [isSearchFocused, setIsSearchFocused] = useState(false);
const memoizedCurrentlyVisibleAssistants = useMemo(() => {
return assistants.filter((assistant) => {
const nameMatches = assistant.name
.toLowerCase()
.includes(searchQuery.toLowerCase());
const labelMatches = assistant.labels?.some((label) =>
label.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const publicFilter =
!assistantFilters[AssistantFilter.Public] || assistant.is_public;
const privateFilter =
!assistantFilters[AssistantFilter.Private] || !assistant.is_public;
const pinnedFilter =
!assistantFilters[AssistantFilter.Pinned] ||
(pinnedAssistants.map((a) => a.id).includes(assistant.id) ?? false);
const mineFilter =
!assistantFilters[AssistantFilter.Mine] ||
checkUserOwnsAssistant(user, assistant);
const isNotUnifiedAssistant = assistant.id !== 0;
return (
(nameMatches || labelMatches) &&
publicFilter &&
privateFilter &&
pinnedFilter &&
mineFilter &&
isNotUnifiedAssistant
);
});
}, [assistants, searchQuery, assistantFilters]);
const featuredAssistants = [
...memoizedCurrentlyVisibleAssistants.filter(
(assistant) => assistant.is_default_persona
),
];
const allAssistants = memoizedCurrentlyVisibleAssistants.filter(
(assistant) => !assistant.is_default_persona
);
return (
<div
onClick={hideModal}
className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50"
>
<div
onClick={(e) => e.stopPropagation()}
className="p-0
max-w-4xl
overflow-hidden
max-h-[80vh]
w-[95%]
bg-background
rounded-md
shadow-2xl
transform
transition-all
duration-300
ease-in-out
relative
w-11/12
pt-10
pb-10
px-10
flex
flex-col"
style={{
position: "fixed",
top: "10vh",
left: "50%",
transform: "translateX(-50%)",
margin: 0,
}}
aria-label="Assistant Modal"
>
<div className="absolute top-2 right-2">
<button
onClick={hideModal}
className="cursor-pointer text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-300 transition-colors duration-200 p-2"
aria-label="Close modal"
>
<XIcon className="w-5 h-5" />
</button>
</div>
<div className="flex overflow-hidden flex-col h-full">
<div className="flex overflow-hidden flex-col h-full">
<div className="flex flex-col sticky top-0 z-10">
<div className="flex px-2 justify-between items-center gap-x-2 mb-0">
<div className="h-12 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
<div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-background-300 flex items-center px-3">
{!isSearchFocused && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-text-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
)}
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
type="text"
className="w-full h-full bg-transparent outline-none text-black"
/>
</div>
</div>
<button
onClick={() => router.push("/assistants/new")}
className="h-10 cursor-pointer px-6 py-3 bg-background-800 hover:bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-text-50 text-lg font-normal leading-normal">
Create
</div>
</button>
</div>
<div className="px-2 flex py-4 items-center gap-x-2 flex-wrap">
<FilterIcon className="text-text-800" size={16} />
<AssistantBadgeSelector
text="Pinned"
selected={assistantFilters[AssistantFilter.Pinned]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Pinned)
}
/>
<AssistantBadgeSelector
text="Mine"
selected={assistantFilters[AssistantFilter.Mine]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Mine)
}
/>
<AssistantBadgeSelector
text="Private"
selected={assistantFilters[AssistantFilter.Private]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Private)
}
/>
<AssistantBadgeSelector
text="Public"
selected={assistantFilters[AssistantFilter.Public]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Public)
}
/>
</div>
<div className="w-full border-t border-background-200" />
</div>
<div className="flex-grow overflow-y-auto">
{featuredAssistants.length === 0 && allAssistants.length === 0 ? (
<div className="flex mt-3 h-96">
<div className="text-center text-text-500 text-sm">
No Assistants configured yet...
</div>
</div>
) : (
<>
{featuredAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-text-800 mb-2 px-4 py-2">
Featured Assistants
</h2>
<div className="w-full px-2 pb-10 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{featuredAssistants.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={pinnedAssistants
.map((a) => a.id)
.includes(assistant.id)}
persona={assistant}
closeModal={hideModal}
/>
</div>
))}
</div>
</>
)}
{allAssistants && allAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-text-800 mt-4 mb-2 px-4 py-2">
All Assistants
</h2>
<div className="w-full mt-2 px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{allAssistants
.sort((a, b) => b.id - a.id)
.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={
user?.preferences?.pinned_assistants?.includes(
assistant.id
) ?? false
}
persona={assistant}
closeModal={hideModal}
/>
</div>
))}
</div>
</>
)}
</>
)}
</div>
</div>
</div>
</div>
</div>
);
}
export default AssistantModal;

View File

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

View File

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

View File

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

View File

@@ -1,39 +0,0 @@
"use client";
import { useChatContext } from "@/components/context/ChatContext";
import { ChatPage } from "./components/ChatPage";
import { useCallback, useState } from "react";
export default function ChatLayout({
firstMessage,
defaultSidebarOff,
}: {
firstMessage?: string;
// This is required for the chrome extension side panel
// we don't want to show the sidebar by default when the user opens the side panel
defaultSidebarOff?: boolean;
}) {
const { sidebarInitiallyVisible } = useChatContext();
const [sidebarVisible, setSidebarVisible] = useState(
(sidebarInitiallyVisible && !defaultSidebarOff) ?? false
);
const toggle = useCallback((value?: boolean) => {
setSidebarVisible((sidebarVisiblePrevValue) =>
value !== undefined ? value : !sidebarVisiblePrevValue
);
}, []);
return (
<>
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
<ChatPage
toggle={toggle}
sidebarVisible={sidebarVisible}
firstMessage={firstMessage}
/>
</div>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +1,113 @@
import React, { RefObject, useCallback, useMemo } from "react";
import { Message } from "../interfaces";
import { OnyxDocument, MinimalOnyxDocument } from "@/lib/search/interfaces";
import { MemoizedHumanMessage } from "../message/MemoizedHumanMessage";
import { ErrorBanner } from "../message/Resubmit";
import { Message } from "@/app/chat/interfaces";
import { OnyxDocument } from "@/lib/search/interfaces";
import { MemoizedHumanMessage } from "@/app/chat/message/MemoizedHumanMessage";
import { ErrorBanner } from "@/app/chat/message/Resubmit";
import { FeedbackType } from "@/app/chat/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { LlmDescriptor } from "@/lib/hooks";
import { LlmDescriptor, useLlmManager } from "@/lib/hooks";
import {
FileResponse,
FolderResponse,
useDocumentsContext,
} from "@/app/chat/my-documents/DocumentsContext";
import { EnterpriseSettings } from "@/app/admin/settings/interfaces";
import { FileDescriptor } from "@/app/chat/interfaces";
import { MemoizedAIMessage } from "../message/messageComponents/MemoizedAIMessage";
import { MemoizedAIMessage } from "@/app/chat/message/messageComponents/MemoizedAIMessage";
import {
useCurrentMessageHistory,
useCurrentMessageTree,
useLoadingError,
} from "../stores/useChatSessionStore";
import { useChatContext } from "@/components/context/ChatContext";
import { useAgentsContext } from "@/components-2/context/AgentsContext";
import { useDeepResearchToggle } from "../hooks/useDeepResearchToggle";
interface MessagesDisplayProps {
messageHistory: Message[];
completeMessageTree: Map<number, Message> | null | undefined;
liveAssistant: MinimalPersonaSnapshot;
llmManager: { currentLlm: LlmDescriptor | null };
deepResearchEnabled: boolean;
interface RegenerationRequest {
messageId: number;
parentMessage: Message;
forceSearch?: boolean;
}
interface OnSubmitArgs {
message: string;
messageIdToResend?: number;
selectedFiles: FileResponse[];
selectedFolders: FolderResponse[];
currentMessageFiles: FileDescriptor[];
setPresentingDocument: (doc: MinimalOnyxDocument | null) => void;
useAgentSearch: boolean;
modelOverride?: LlmDescriptor;
regenerationRequest?: RegenerationRequest;
forceSearch?: boolean;
queryOverride?: string;
isSeededChat?: boolean;
overrideFileDescriptors?: FileDescriptor[];
}
interface MessagesDisplayProps {
setCurrentFeedback: (feedback: [FeedbackType, number] | null) => void;
onSubmit: (args: {
message: string;
messageIdToResend?: number;
selectedFiles: FileResponse[];
selectedFolders: FolderResponse[];
currentMessageFiles: FileDescriptor[];
useAgentSearch: boolean;
modelOverride?: LlmDescriptor;
regenerationRequest?: {
messageId: number;
parentMessage: Message;
forceSearch?: boolean;
};
forceSearch?: boolean;
queryOverride?: string;
isSeededChat?: boolean;
overrideFileDescriptors?: FileDescriptor[];
}) => Promise<void>;
onSubmit: (args: OnSubmitArgs) => Promise<void>;
onMessageSelection: (nodeId: number) => void;
stopGenerating: () => void;
uncaughtError: string | null;
loadingError: string | null;
handleResubmitLastMessage: () => void;
autoScrollEnabled: boolean;
getContainerHeight: () => string | undefined;
// autoScrollEnabled: boolean;
lastMessageRef: RefObject<HTMLDivElement>;
endPaddingRef: RefObject<HTMLDivElement>;
endDivRef: RefObject<HTMLDivElement>;
hasPerformedInitialScroll: boolean;
chatSessionId: string | null;
enterpriseSettings?: EnterpriseSettings | null;
// hasPerformedInitialScroll: boolean;
// chatSessionId: string | null;
// enterpriseSettings?: EnterpriseSettings | null;
}
export const MessagesDisplay: React.FC<MessagesDisplayProps> = ({
messageHistory,
completeMessageTree,
liveAssistant,
llmManager,
deepResearchEnabled,
selectedFiles,
selectedFolders,
currentMessageFiles,
setPresentingDocument,
export function MessagesDisplay({
setCurrentFeedback,
onSubmit,
onMessageSelection,
stopGenerating,
uncaughtError,
loadingError,
// loadingError,
handleResubmitLastMessage,
autoScrollEnabled,
getContainerHeight,
// autoScrollEnabled,
lastMessageRef,
endPaddingRef,
endDivRef,
hasPerformedInitialScroll,
chatSessionId,
enterpriseSettings,
}) => {
// hasPerformedInitialScroll,
// chatSessionId,
// enterpriseSettings,
}: MessagesDisplayProps) {
const { currentChat, currentChatId, llmProviders } = useChatContext();
const messageHistory = useCurrentMessageHistory();
const completeMessageTree = useCurrentMessageTree();
const {
currentAgent: currentAgentOrNone,
unifiedAgent,
pinnedAgents,
agents,
} = useAgentsContext();
const currentAgent =
currentAgentOrNone || unifiedAgent || pinnedAgents?.[0] || agents?.[0];
if (!currentAgent)
throw new Error(
"At least one agent should be specified before reaching this point"
);
const { deepResearchEnabled } = useDeepResearchToggle({
chatSessionId: currentChatId,
assistantId: currentAgent?.id,
});
const llmManager = useLlmManager(llmProviders, currentChat!, currentAgent!);
const {
selectedFiles,
selectedFolders,
currentMessageFiles,
setPresentingDocument,
} = useDocumentsContext();
const loadingError = useLoadingError();
// Stable fallbacks to avoid changing prop identities on each render
const emptyDocs = useMemo<OnyxDocument[]>(() => [], []);
const emptyChildrenIds = useMemo<number[]>(() => [], []);
const createRegenerator = useCallback(
(regenerationRequest: {
messageId: number;
parentMessage: Message;
forceSearch?: boolean;
}) => {
(regenerationRequest: RegenerationRequest) => {
return async function (modelOverride: LlmDescriptor) {
return await onSubmit({
message: regenerationRequest.parentMessage.message,
@@ -138,96 +153,93 @@ export const MessagesDisplay: React.FC<MessagesDisplayProps> = ({
);
return (
<div
style={{ overflowAnchor: "none" }}
key={chatSessionId}
className={
(hasPerformedInitialScroll ? "" : " hidden ") +
"desktop:-ml-4 w-full mx-auto " +
"absolute mobile:top-0 desktop:top-0 left-0 " +
(enterpriseSettings?.two_lines_for_chat_header ? "pt-20 " : "pt-4 ")
}
>
{messageHistory.map((message, i) => {
const messageTree = completeMessageTree;
const messageReactComponentKey = `message-${message.nodeId}`;
const parentMessage = message.parentNodeId
? messageTree?.get(message.parentNodeId)
: null;
<>
<div>
{messageHistory.map((message, index) => {
const messageTree = completeMessageTree;
const messageReactComponentKey = `message-${message.nodeId}`;
const parentMessage = message.parentNodeId
? messageTree?.get(message.parentNodeId)
: null;
if (message.type === "user") {
const nextMessage =
messageHistory.length > i + 1 ? messageHistory[i + 1] : null;
if (message.type === "user") {
const nextMessage =
messageHistory.length > index + 1
? messageHistory[index + 1]
: null;
return (
<div id={messageReactComponentKey} key={messageReactComponentKey}>
<MemoizedHumanMessage
setPresentingDocument={setPresentingDocument}
disableSwitchingForStreaming={
(nextMessage && nextMessage.is_generating) || false
}
stopGenerating={stopGenerating}
content={message.message}
files={message.files}
messageId={message.messageId}
handleEditWithMessageId={handleEditWithMessageId}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
/>
</div>
);
} else if (message.type === "assistant") {
if (
(uncaughtError || loadingError) &&
i === messageHistory.length - 1
) {
return (
<div id={messageReactComponentKey} key={messageReactComponentKey}>
<MemoizedHumanMessage
setPresentingDocument={setPresentingDocument}
disableSwitchingForStreaming={
(nextMessage && nextMessage.is_generating) || false
}
stopGenerating={stopGenerating}
content={message.message}
files={message.files}
messageId={message.messageId}
handleEditWithMessageId={handleEditWithMessageId}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
/>
</div>
);
} else if (message.type === "assistant") {
if (
(uncaughtError || loadingError) &&
index === messageHistory.length - 1
) {
return (
<div
key={`error-${message.nodeId}`}
className="max-w-message-max mx-auto"
>
<ErrorBanner
resubmit={handleResubmitLastMessage}
error={uncaughtError || loadingError || ""}
/>
</div>
);
}
// NOTE: it's fine to use the previous entry in messageHistory
// since this is a "parsed" version of the message tree
// so the previous message is guaranteed to be the parent of the current message
const previousMessage =
index !== 0 ? messageHistory[index - 1] : null;
return (
<div
key={`error-${message.nodeId}`}
className="max-w-message-max mx-auto"
id={`message-${message.nodeId}`}
key={messageReactComponentKey}
ref={
index === messageHistory.length - 1 ? lastMessageRef : null
}
>
<ErrorBanner
resubmit={handleResubmitLastMessage}
error={uncaughtError || loadingError || ""}
<MemoizedAIMessage
rawPackets={message.packets}
handleFeedbackWithMessageId={handleFeedback}
assistant={currentAgent}
docs={message.documents ?? emptyDocs}
citations={message.citations}
setPresentingDocument={setPresentingDocument}
createRegenerator={createRegenerator}
parentMessage={previousMessage!}
messageId={message.messageId}
overriddenModel={llmManager.currentLlm?.modelName}
nodeId={message.nodeId}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
/>
</div>
);
}
// NOTE: it's fine to use the previous entry in messageHistory
// since this is a "parsed" version of the message tree
// so the previous message is guaranteed to be the parent of the current message
const previousMessage = i !== 0 ? messageHistory[i - 1] : null;
return (
<div
className="text-text"
id={`message-${message.nodeId}`}
key={messageReactComponentKey}
ref={i === messageHistory.length - 1 ? lastMessageRef : null}
>
<MemoizedAIMessage
rawPackets={message.packets}
handleFeedbackWithMessageId={handleFeedback}
assistant={liveAssistant}
docs={message.documents ?? emptyDocs}
citations={message.citations}
setPresentingDocument={setPresentingDocument}
createRegenerator={createRegenerator}
parentMessage={previousMessage!}
messageId={message.messageId}
overriddenModel={llmManager.currentLlm?.modelName}
nodeId={message.nodeId}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
/>
</div>
);
}
})}
})}
</div>
{((uncaughtError !== null || loadingError !== null) &&
messageHistory[messageHistory.length - 1]?.type === "user") ||
@@ -240,16 +252,7 @@ export const MessagesDisplay: React.FC<MessagesDisplayProps> = ({
</div>
))}
{messageHistory.length > 0 && (
<div
style={{
height: !autoScrollEnabled ? getContainerHeight() : undefined,
}}
/>
)}
<div ref={endPaddingRef} className="h-[95px]" />
<div ref={endDivRef} />
</div>
</>
);
};
}

View File

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

View File

@@ -1,361 +0,0 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import { Folder } from "./interfaces";
import { ChatSessionDisplay } from "@/components/sidebar/ChatSessionDisplay"; // Ensure this is correctly imported
import {
FiChevronDown,
FiChevronRight,
FiFolder,
FiEdit2,
FiCheck,
FiX,
FiTrash, // Import the trash icon
} from "react-icons/fi";
import { BasicSelectable } from "@/components/BasicClickable";
import {
addChatToFolder,
deleteFolder,
updateFolderName,
} from "./FolderManagement";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
import Cookies from "js-cookie";
import { Popover } from "@/components/popover/Popover";
import { ChatSession } from "@/app/chat/interfaces";
import { useChatContext } from "@/components/context/ChatContext";
const FolderItem = ({
folder,
currentChatId,
isInitiallyExpanded,
initiallySelected,
showShareModal,
showDeleteModal,
}: {
folder: Folder;
currentChatId?: string;
isInitiallyExpanded: boolean;
initiallySelected: boolean;
showShareModal: ((chatSession: ChatSession) => void) | undefined;
showDeleteModal: ((chatSession: ChatSession) => void) | undefined;
}) => {
const { refreshChatSessions } = useChatContext();
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
const [isEditing, setIsEditing] = useState<boolean>(initiallySelected);
const [editedFolderName, setEditedFolderName] = useState<string>(
folder.folder_name
);
const [isHovering, setIsHovering] = useState<boolean>(false);
const [isDragOver, setIsDragOver] = useState<boolean>(false);
const { setPopup } = usePopup();
const router = useRouter();
const toggleFolderExpansion = () => {
if (!isEditing) {
const newIsExpanded = !isExpanded;
setIsExpanded(newIsExpanded);
// Update the cookie with the new state
const openedFoldersCookieVal = Cookies.get("openedFolders");
const openedFolders = openedFoldersCookieVal
? JSON.parse(openedFoldersCookieVal)
: {};
if (newIsExpanded) {
openedFolders[folder.folder_id!] = true;
} else {
setShowDeleteConfirm(false);
delete openedFolders[folder.folder_id!];
}
Cookies.set("openedFolders", JSON.stringify(openedFolders));
}
};
const handleEditFolderName = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation(); // Prevent the event from bubbling up to the toggle expansion
setIsEditing(true);
};
const handleFolderNameChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setEditedFolderName(event.target.value);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
saveFolderName();
}
};
const saveFolderName = async (continueEditing?: boolean) => {
try {
await updateFolderName(folder.folder_id!, editedFolderName);
if (!continueEditing) {
setIsEditing(false);
}
router.refresh(); // Refresh values to update the sidebar
} catch (error) {
setPopup({ message: "Failed to save folder name", type: "error" });
}
};
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const deleteConfirmRef = useRef<HTMLDivElement>(null);
const handleDeleteClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
setShowDeleteConfirm(true);
};
const confirmDelete = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
try {
await deleteFolder(folder.folder_id!);
router.refresh();
} catch (error) {
setPopup({ message: "Failed to delete folder", type: "error" });
} finally {
setShowDeleteConfirm(false);
}
};
const cancelDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
setShowDeleteConfirm(false);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
deleteConfirmRef.current &&
!deleteConfirmRef.current.contains(event.target as Node)
) {
setShowDeleteConfirm(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (initiallySelected && inputRef.current) {
inputRef.current.focus();
}
}, [initiallySelected]);
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
const chatSessionId = event.dataTransfer.getData(CHAT_SESSION_ID_KEY);
try {
await addChatToFolder(folder.folder_id!, chatSessionId);
await refreshChatSessions();
router.refresh();
} catch (error) {
setPopup({
message: "Failed to add chat session to folder",
type: "error",
});
}
};
const folders = folder.chat_sessions.sort((a, b) => {
return a.time_updated.localeCompare(b.time_updated);
});
// Determine whether to show the trash can icon
const showTrashIcon = (isHovering && !isEditing) || showDeleteConfirm;
return (
<div
key={folder.folder_id}
onDragOver={(event) => {
event.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDrop}
className={`transition duration-300 ease-in-out rounded-md ${
isDragOver ? "bg-accent-background-hovered" : ""
}`}
>
<BasicSelectable fullWidth selected={false}>
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div onClick={toggleFolderExpansion} className="cursor-pointer">
<div className="text-sm text-text-600 flex items-center justify-start w-full">
<div className="mr-2">
{isExpanded ? (
<FiChevronDown size={16} />
) : (
<FiChevronRight size={16} />
)}
</div>
<div>
<FiFolder size={16} className="mr-2" />
</div>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={editedFolderName}
onChange={handleFolderNameChange}
onKeyDown={handleKeyDown}
onBlur={() => saveFolderName(true)}
className="text-sm px-1 flex-1 min-w-0 -my-px mr-2"
/>
) : (
<div className="flex-1 break-all min-w-0">
{editedFolderName || folder.folder_name}
</div>
)}
<div className="flex ml-auto my-auto">
<div
onClick={handleEditFolderName}
className={`hover:bg-black/10 p-1 -m-1 rounded ${
isHovering && !isEditing
? ""
: "opacity-0 pointer-events-none"
}`}
>
<FiEdit2 size={16} />
</div>
<div className="relative">
<Popover
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
content={
<div
onClick={handleDeleteClick}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2 ${
showTrashIcon ? "" : "opacity-0 pointer-events-none"
}`}
>
<FiTrash size={16} />
</div>
}
popover={
<div className="p-2 w-[225px] bg-background-100 rounded shadow-lg">
<p className="text-sm mb-2">
Are you sure you want to delete folder{" "}
<i>{folder.folder_name}</i>?
</p>
<div className="flex justify-end">
<button
onClick={confirmDelete}
className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs mr-2"
>
Yes
</button>
<button
onClick={cancelDelete}
className="bg-background-300 hover:bg-background-200 px-2 py-1 rounded text-xs"
>
No
</button>
</div>
</div>
}
side="top"
align="center"
/>
</div>
</div>
{isEditing && (
<div className="flex ml-auto my-auto">
<div
onClick={() => saveFolderName()}
className="hover:bg-black/10 p-1 -m-1 rounded"
>
<FiCheck size={16} />
</div>
<div
onClick={() => setIsEditing(false)}
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
>
<FiX size={16} />
</div>
</div>
)}
</div>
</div>
</div>
</BasicSelectable>
{/* Expanded Folder Content */}
{isExpanded && folders && (
<div className={"mr-4 pl-2 w-full border-l border-border"}>
{folders.map((chatSession) => (
<ChatSessionDisplay
key={chatSession.id}
chatSession={chatSession}
isSelected={chatSession.id === currentChatId}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
parentFolderName={folder.folder_name}
/>
))}
</div>
)}
</div>
);
};
export const FolderList = ({
folders,
currentChatId,
openedFolders,
newFolderId,
showShareModal,
showDeleteModal,
}: {
folders: Folder[];
currentChatId?: string;
openedFolders?: { [key: number]: boolean };
newFolderId: number | null;
showShareModal: ((chatSession: ChatSession) => void) | undefined;
showDeleteModal: ((chatSession: ChatSession) => void) | undefined;
}) => {
if (folders.length === 0) {
return null;
}
return (
<div className="mt-1 mb-1 overflow-visible">
{folders.map((folder) => (
<FolderItem
key={folder.folder_id}
folder={folder}
currentChatId={currentChatId}
initiallySelected={newFolderId == folder.folder_id}
isInitiallyExpanded={
openedFolders ? openedFolders[folder.folder_id!] || false : false
}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
/>
))}
{folders.length == 1 &&
folders[0] &&
folders[0].chat_sessions.length == 0 && (
<p className="text-sm font-normal text-subtle mt-2">
{" "}
Drag a chat into a folder to save for later{" "}
</p>
)}
</div>
);
};

View File

@@ -1,97 +0,0 @@
// Function to create a new folder
export async function createFolder(folderName: string): Promise<number> {
const response = await fetch("/api/folder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder_name: folderName }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Failed to create folder");
}
const data = await response.json();
return data;
}
// Function to add a chat session to a folder
export async function addChatToFolder(
folderId: number,
chatSessionId: string
): Promise<void> {
const response = await fetch(`/api/folder/${folderId}/add-chat-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ chat_session_id: chatSessionId }),
});
if (!response.ok) {
throw new Error("Failed to add chat to folder");
}
}
// Function to remove a chat session from a folder
export async function removeChatFromFolder(
folderId: number,
chatSessionId: string
): Promise<void> {
const response = await fetch(`/api/folder/${folderId}/remove-chat-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ chat_session_id: chatSessionId }),
});
if (!response.ok) {
throw new Error("Failed to remove chat from folder");
}
}
// Function to delete a folder
export async function deleteFolder(folderId: number): Promise<void> {
const response = await fetch(`/api/folder/${folderId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error("Failed to delete folder");
}
}
// Function to update a folder's name
export async function updateFolderName(
folderId: number,
newName: string
): Promise<void> {
const response = await fetch(`/api/folder/${folderId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder_name: newName }),
});
if (!response.ok) {
throw new Error("Failed to update folder name");
}
}
// Function to update folder display priorities
export async function updateFolderDisplayPriorities(
displayPriorityMap: Record<number, number>
): Promise<void> {
const response = await fetch(`/api/folder/reorder`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ display_priority_map: displayPriorityMap }),
});
if (!response.ok) {
throw new Error("Failed to update folder display priorities");
}
}

View File

@@ -7,14 +7,13 @@ import React, {
} from "react";
import { FiPlus } from "react-icons/fi";
import { FiLoader } from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption";
import { ChatInputOption } from "@/app/chat/components/input/ChatInputOption";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import LLMPopover from "./LLMPopover";
import LLMPopover from "@/app/chat/components/input/LLMPopover";
import { InputPrompt } from "@/app/chat/interfaces";
import { FilterManager, LlmManager } from "@/lib/hooks";
import { useChatContext } from "@/components/context/ChatContext";
import { ChatFileType } from "../../interfaces";
import { ChatFileType } from "@/app/chat/interfaces";
import {
DocumentIcon2,
FileIcon,
@@ -34,61 +33,62 @@ import { useUser } from "@/components/user/UserProvider";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useDocumentsContext } from "@/app/chat/my-documents/DocumentsContext";
import { UnconfiguredLlmProviderText } from "@/components/chat/UnconfiguredLlmProviderText";
import { DeepResearchToggle } from "./DeepResearchToggle";
import { ActionToggle } from "./ActionManagement";
import { SelectedTool } from "./SelectedTool";
import { DeepResearchToggle } from "@/app/chat/components/input/DeepResearchToggle";
import { ActionToggle } from "@/app/chat/components/input/ActionManagement";
import { SelectedTool } from "@/app/chat/components/input/SelectedTool";
const MAX_INPUT_HEIGHT = 200;
export const SourceChip = ({
icon,
title,
onRemove,
onClick,
truncateTitle = true,
}: {
export interface SourceChipProps {
icon?: React.ReactNode;
title: string;
onRemove?: () => void;
onClick?: () => void;
truncateTitle?: boolean;
}) => (
<div
onClick={onClick ? onClick : undefined}
className={`
}
export function SourceChip({
icon,
title,
onRemove,
onClick,
truncateTitle = true,
}: SourceChipProps) {
return (
<div
onClick={onClick ? onClick : undefined}
className={`
flex-none
flex
items-center
px-1
bg-background-background
text-xs
text-text-darker
text-text-04
border
gap-x-1.5
border-border
rounded-md
box-border
gap-x-1
h-6
${onClick ? "cursor-pointer" : ""}
`}
>
{icon}
{truncateTitle ? truncateString(title, 20) : title}
{onRemove && (
<XIcon
size={12}
className="text-text-900 ml-auto cursor-pointer"
onClick={(e: React.MouseEvent<SVGSVGElement>) => {
e.stopPropagation();
onRemove();
}}
/>
)}
</div>
);
>
{icon}
{truncateTitle ? truncateString(title, 20) : title}
{onRemove && (
<XIcon
size={12}
className="text-text-05 ml-auto cursor-pointer"
onClick={(e: React.MouseEvent<SVGSVGElement>) => {
e.stopPropagation();
onRemove();
}}
/>
)}
</div>
);
}
interface ChatInputBarProps {
export interface ChatInputBarProps {
toggleDocSelection: () => void;
removeDocs: () => void;
showConfigureAPIKey: () => void;
@@ -113,7 +113,7 @@ interface ChatInputBarProps {
placeholder?: string;
}
export const ChatInputBar = React.memo(function ChatInputBar({
function ChatInputBarInner({
toggleDocSelection,
retrievalEnabled,
removeDocs,
@@ -325,97 +325,82 @@ export const ChatInputBar = React.memo(function ChatInputBar({
}, [selectedFiles, currentMessageFiles, currentMessageFileIds]);
return (
<div id="onyx-chat-input">
<div className="flex justify-center mx-auto">
<div id="onyx-chat-input" className="flex justify-center mx-auto">
<div className="w-[800px] relative desktop:px-4 mx-auto">
{showPrompts && user?.preferences?.shortcut_enabled && (
<div className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full">
<div className="rounded-lg overflow-y-auto max-h-[200px] py-1.5 bg-background-neutral-00 shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPrompts.map(
(currentPrompt: InputPrompt, index: number) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-background-neutral-02/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-neutral-02/90 cursor-pointer`}
onClick={() => {
updateInputPrompt(currentPrompt);
}}
>
<p className="font-bold">{currentPrompt.prompt}:</p>
<p className="text-left flex-grow mr-auto line-clamp-1">
{currentPrompt.content?.trim()}
</p>
</button>
)
)}
<a
key={filteredPrompts.length}
target="_self"
className={`${
tabbingIconIndex == filteredPrompts.length &&
"bg-background-neutral-02/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-neutral-02/90 cursor-pointer`}
href="/chat/input-prompts"
>
<FiPlus size={17} />
<p>Create a new prompt</p>
</a>
</div>
</div>
)}
<UnconfiguredLlmProviderText
showConfigureAPIKey={showConfigureAPIKey}
/>
<div
className="
max-w-full
w-[800px]
relative
desktop:px-4
mx-auto
"
>
{showPrompts && user?.preferences?.shortcut_enabled && (
<div className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full">
<div className="rounded-lg overflow-y-auto max-h-[200px] py-1.5 bg-input-background dark:border-none border border-border shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPrompts.map(
(currentPrompt: InputPrompt, index: number) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index &&
"bg-background-dark/75 dark:bg-neutral-800/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-dark/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
onClick={() => {
updateInputPrompt(currentPrompt);
}}
>
<p className="font-bold">{currentPrompt.prompt}:</p>
<p className="text-left flex-grow mr-auto line-clamp-1">
{currentPrompt.content?.trim()}
</p>
</button>
)
)}
<a
key={filteredPrompts.length}
target="_self"
className={`${
tabbingIconIndex == filteredPrompts.length &&
"bg-background-dark/75 dark:bg-neutral-800/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-dark/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
href="/chat/input-prompts"
>
<FiPlus size={17} />
<p>Create a new prompt</p>
</a>
</div>
</div>
)}
<UnconfiguredLlmProviderText
showConfigureAPIKey={showConfigureAPIKey}
/>
<div className="w-full h-[10px]"></div>
<div
className="
opacity-100
w-full
h-fit
flex
flex-col
border
shadow-lg
bg-input-background
border-input-border
dark:border-none
bg-background-neutral-00
rounded-xl
overflow-hidden
text-text-chatbar
text-text-04
[&:has(textarea:focus)]::ring-1
[&:has(textarea:focus)]::ring-black
"
>
<textarea
onPaste={handlePaste}
onKeyDownCapture={handleKeyDown}
onChange={handleInputChange}
ref={textAreaRef}
id="onyx-chat-input-textarea"
className={`
>
<textarea
onPaste={handlePaste}
onKeyDownCapture={handleKeyDown}
onChange={handleInputChange}
ref={textAreaRef}
id="onyx-chat-input-textarea"
className={`
m-0
w-full
shrink
resize-none
rounded-lg
border-0
bg-input-background
bg-background-neutral-00
font-normal
text-base
leading-6
placeholder:text-text-400 dark:placeholder:text-text-500
placeholder:text-text-02
${
textAreaRef.current &&
textAreaRef.current.scrollHeight > MAX_INPUT_HEIGHT
@@ -430,284 +415,286 @@ export const ChatInputBar = React.memo(function ChatInputBar({
px-5
py-5
`}
autoFocus
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder={
placeholder ||
(selectedAssistant.id === 0
? `How can ${settings?.enterpriseSettings?.application_name || "Onyx"} help you today`
: `How can ${selectedAssistant.name} help you today`)
}
value={message}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!showPrompts &&
!event.shiftKey &&
!(event.nativeEvent as any).isComposing
) {
event.preventDefault();
if (message) {
onSubmit();
}
autoFocus
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder={
placeholder ||
(selectedAssistant.id === 0
? `How can ${settings?.enterpriseSettings?.application_name || "Onyx"} help you today`
: `How can ${selectedAssistant.name} help you today`)
}
value={message}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!showPrompts &&
!event.shiftKey &&
!(event.nativeEvent as any).isComposing
) {
event.preventDefault();
if (message) {
onSubmit();
}
}}
suppressContentEditableWarning={true}
/>
}
}}
suppressContentEditableWarning={true}
/>
{(selectedDocuments.length > 0 ||
selectedFiles.length > 0 ||
selectedFolders.length > 0 ||
currentMessageFiles.length > 0 ||
filterManager.timeRange ||
filterManager.selectedDocumentSets.length > 0 ||
filterManager.selectedTags.length > 0 ||
filterManager.selectedSources.length > 0) && (
<div className="flex bg-input-background gap-x-.5 px-2">
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
{filterManager.selectedTags &&
filterManager.selectedTags.map((tag, index) => (
<SourceChip
key={index}
icon={<TagIcon size={12} />}
title={`#${tag.tag_key}_${tag.tag_value}`}
onRemove={() => {
filterManager.setSelectedTags(
filterManager.selectedTags.filter(
(t) => t.tag_key !== tag.tag_key
)
);
}}
/>
))}
{/* Unified file rendering section for both selected and current message files */}
{allFiles.map((file, index) =>
file.chatFileType === ChatFileType.IMAGE ? (
<SourceChip
key={`${file.source}-${file.id}-${index}`}
icon={
file.isUploading ? (
<FiLoader className="animate-spin" />
) : (
<img
className="h-full py-.5 object-cover rounded-lg bg-background cursor-pointer"
src={buildImgUrl(file.id)}
alt={file.name || "File image"}
/>
)
}
title={file.name}
onRemove={() => {
if (file.source === "selected") {
removeSelectedFile(file.originalFile);
} else {
setCurrentMessageFiles(
currentMessageFiles.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}
}}
/>
) : (
<SourceChip
key={`${file.source}-${file.id}-${index}`}
icon={
<FileIcon
className={
file.source === "current" ? "text-red-500" : ""
}
size={16}
/>
}
title={file.name}
onRemove={() => {
if (file.source === "selected") {
removeSelectedFile(file.originalFile);
} else {
setCurrentMessageFiles(
currentMessageFiles.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}
}}
/>
)
)}
{selectedFolders.map((folder) => (
{(selectedDocuments.length > 0 ||
selectedFiles.length > 0 ||
selectedFolders.length > 0 ||
currentMessageFiles.length > 0 ||
filterManager.timeRange ||
filterManager.selectedDocumentSets.length > 0 ||
filterManager.selectedTags.length > 0 ||
filterManager.selectedSources.length > 0) && (
<div className="flex bg-background-neutral-01 gap-x-.5 px-2">
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
{filterManager.selectedTags &&
filterManager.selectedTags.map((tag, index) => (
<SourceChip
key={folder.id}
icon={<FolderIcon size={16} />}
title={folder.name}
onRemove={() => removeSelectedFolder(folder)}
key={index}
icon={<TagIcon size={12} />}
title={`#${tag.tag_key}_${tag.tag_value}`}
onRemove={() => {
filterManager.setSelectedTags(
filterManager.selectedTags.filter(
(t) => t.tag_key !== tag.tag_key
)
);
}}
/>
))}
{filterManager.timeRange && (
{/* Unified file rendering section for both selected and current message files */}
{allFiles.map((file, index) =>
file.chatFileType === ChatFileType.IMAGE ? (
<SourceChip
truncateTitle={false}
key="time-range"
icon={<CalendarIcon size={12} />}
title={`${getFormattedDateRangeString(
filterManager.timeRange.from,
filterManager.timeRange.to
)}`}
onRemove={() => {
filterManager.setTimeRange(null);
}}
/>
)}
{filterManager.selectedDocumentSets.length > 0 &&
filterManager.selectedDocumentSets.map((docSet, index) => (
<SourceChip
key={`doc-set-${index}`}
icon={<DocumentIcon2 size={16} />}
title={docSet}
onRemove={() => {
filterManager.setSelectedDocumentSets(
filterManager.selectedDocumentSets.filter(
(ds) => ds !== docSet
)
);
}}
/>
))}
{filterManager.selectedSources.length > 0 &&
filterManager.selectedSources.map((source, index) => (
<SourceChip
key={`source-${index}`}
icon={
<SourceIcon
sourceType={source.internalName}
iconSize={16}
key={`${file.source}-${file.id}-${index}`}
icon={
file.isUploading ? (
<FiLoader className="animate-spin" />
) : (
<img
className="h-full py-.5 object-cover rounded-lg bg-background-neutral-00 cursor-pointer"
src={buildImgUrl(file.id)}
alt={file.name || "File image"}
/>
}
title={source.displayName}
onRemove={() => {
filterManager.setSelectedSources(
filterManager.selectedSources.filter(
(s) => s.internalName !== source.internalName
)
}
title={file.name}
onRemove={() => {
if (file.source === "selected") {
removeSelectedFile(file.originalFile);
} else {
setCurrentMessageFiles(
currentMessageFiles.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}}
/>
))}
{selectedDocuments.length > 0 && (
<SourceChip
key="selected-documents"
onClick={() => {
toggleDocumentSidebar();
}
}}
icon={<FileIcon size={16} />}
title={`${selectedDocuments.length} selected`}
onRemove={removeDocs}
/>
)}
</div>
</div>
)}
<div className="flex pr-4 pb-2 justify-between bg-input-background items-center w-full ">
<div className="space-x-1 flex px-4 ">
<ChatInputOption
flexPriority="stiff"
Icon={FileUploadIcon}
onClick={() => {
toggleDocSelection();
}}
tooltipContent={"Upload files and attach user files"}
/>
{selectedAssistant.tools.length > 0 && (
<ActionToggle selectedAssistant={selectedAssistant} />
)}
{retrievalEnabled &&
settings?.settings.deep_research_enabled && (
<DeepResearchToggle
deepResearchEnabled={deepResearchEnabled}
toggleDeepResearch={toggleDeepResearch}
/>
)}
{forcedToolIds.length > 0 && (
<div className="pl-1 flex items-center gap-2 text-blue-500">
{forcedToolIds.map((toolId) => {
const tool = selectedAssistant.tools.find(
(tool) => tool.id === toolId
);
if (!tool) {
return null;
}
return (
<SelectedTool
key={toolId}
tool={tool}
onClick={() => {
setForcedToolIds((prev) =>
prev.filter((id) => id !== toolId)
);
}}
/>
);
})}
</div>
)}
</div>
<div className="flex items-center my-auto gap-x-2">
<LLMPopover
llmProviders={llmProviders}
llmManager={llmManager}
requiresImageGeneration={false}
currentAssistant={selectedAssistant}
/>
<button
id="onyx-chat-input-send-button"
className={`cursor-pointer ${
chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading"
? chatState != "streaming"
? "bg-neutral-500 dark:bg-neutral-400 "
: "bg-neutral-900 dark:bg-neutral-50"
: "bg-red-200"
} h-[22px] w-[22px] rounded-full`}
onClick={() => {
if (chatState == "streaming") {
stopGenerating();
} else if (message) {
onSubmit();
}
}}
>
{chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading" ? (
<StopGeneratingIcon
size={8}
className="text-neutral-50 dark:text-neutral-900 m-auto text-white flex-none"
/>
) : (
<SendIcon
size={22}
className={`text-neutral-50 dark:text-neutral-900 p-1 my-auto rounded-full ${
chatState == "input" && message
? "bg-neutral-900 dark:bg-neutral-50"
: "bg-neutral-500 dark:bg-neutral-400"
}`}
<SourceChip
key={`${file.source}-${file.id}-${index}`}
icon={
<FileIcon
className={
file.source === "current"
? "text-status-error-05"
: ""
}
size={16}
/>
}
title={file.name}
onRemove={() => {
if (file.source === "selected") {
removeSelectedFile(file.originalFile);
} else {
setCurrentMessageFiles(
currentMessageFiles.filter(
(fileInFilter) => fileInFilter.id !== file.id
)
);
}
}}
/>
)}
</button>
)
)}
{selectedFolders.map((folder) => (
<SourceChip
key={folder.id}
icon={<FolderIcon size={16} />}
title={folder.name}
onRemove={() => removeSelectedFolder(folder)}
/>
))}
{filterManager.timeRange && (
<SourceChip
truncateTitle={false}
key="time-range"
icon={<CalendarIcon size={12} />}
title={`${getFormattedDateRangeString(
filterManager.timeRange.from,
filterManager.timeRange.to
)}`}
onRemove={() => {
filterManager.setTimeRange(null);
}}
/>
)}
{filterManager.selectedDocumentSets.length > 0 &&
filterManager.selectedDocumentSets.map((docSet, index) => (
<SourceChip
key={`doc-set-${index}`}
icon={<DocumentIcon2 size={16} />}
title={docSet}
onRemove={() => {
filterManager.setSelectedDocumentSets(
filterManager.selectedDocumentSets.filter(
(ds) => ds !== docSet
)
);
}}
/>
))}
{filterManager.selectedSources.length > 0 &&
filterManager.selectedSources.map((source, index) => (
<SourceChip
key={`source-${index}`}
icon={
<SourceIcon
sourceType={source.internalName}
iconSize={16}
/>
}
title={source.displayName}
onRemove={() => {
filterManager.setSelectedSources(
filterManager.selectedSources.filter(
(s) => s.internalName !== source.internalName
)
);
}}
/>
))}
{selectedDocuments.length > 0 && (
<SourceChip
key="selected-documents"
onClick={() => {
toggleDocumentSidebar();
}}
icon={<FileIcon size={16} />}
title={`${selectedDocuments.length} selected`}
onRemove={removeDocs}
/>
)}
</div>
</div>
)}
<div className="flex pr-4 pb-2 justify-between bg-background-neutral-00 items-center w-full ">
<div className="space-x-1 flex px-4 ">
<ChatInputOption
flexPriority="stiff"
Icon={FileUploadIcon}
onClick={() => {
toggleDocSelection();
}}
tooltipContent={"Upload files and attach user files"}
/>
{selectedAssistant.tools.length > 0 && (
<ActionToggle selectedAssistant={selectedAssistant} />
)}
{retrievalEnabled && settings?.settings.deep_research_enabled && (
<DeepResearchToggle
deepResearchEnabled={deepResearchEnabled}
toggleDeepResearch={toggleDeepResearch}
/>
)}
{forcedToolIds.length > 0 && (
<div className="pl-1 flex items-center gap-2 text-status-info-05">
{forcedToolIds.map((toolId) => {
const tool = selectedAssistant.tools.find(
(tool) => tool.id === toolId
);
if (!tool) {
return null;
}
return (
<SelectedTool
key={toolId}
tool={tool}
onClick={() => {
setForcedToolIds((prev) =>
prev.filter((id) => id !== toolId)
);
}}
/>
);
})}
</div>
)}
</div>
<div className="flex items-center my-auto gap-x-2">
<LLMPopover
llmProviders={llmProviders}
llmManager={llmManager}
requiresImageGeneration={false}
currentAssistant={selectedAssistant}
/>
<button
id="onyx-chat-input-send-button"
className={`cursor-pointer ${
chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading"
? chatState != "streaming"
? "bg-background-neutral-03"
: "bg-background-neutral-05"
: "bg-theme-primary-05"
} h-[22px] w-[22px] rounded-full`}
onClick={() => {
if (chatState == "streaming") {
stopGenerating();
} else if (message) {
onSubmit();
}
}}
>
{chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading" ? (
<StopGeneratingIcon
size={8}
className="text-text-inverted-01 m-auto text-text-inverted-01 flex-none"
/>
) : (
<SendIcon
size={22}
className={`text-text-inverted-01 p-1 my-auto rounded-full ${
chatState == "input" && message
? "bg-background-neutral-05"
: "bg-background-neutral-03"
}`}
/>
)}
</button>
</div>
</div>
</div>
</div>
</div>
);
});
}
export const ChatInputBar = React.memo(ChatInputBarInner);

View File

@@ -22,7 +22,7 @@ import { FiAlertTriangle } from "react-icons/fi";
import { Slider } from "@/components/ui/slider";
import { useUser } from "@/components/user/UserProvider";
import { TruncatedText } from "@/components/ui/truncatedText";
import { ChatInputOption } from "./ChatInputOption";
import { ChatInputOption } from "@/app/chat/components/input/ChatInputOption";
interface LLMPopoverProps {
llmProviders: LLMProviderDescriptor[];
@@ -79,10 +79,7 @@ export default function LLMPopover({
trigger
? () => trigger
: () => (
<button
className="dark:text-[#fff] text-[#000] focus:outline-none"
data-testid="llm-popover-trigger"
>
<button data-testid="llm-popover-trigger">
<ChatInputOption
minimize
toggle
@@ -123,11 +120,16 @@ export default function LLMPopover({
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>{triggerContent}</PopoverTrigger>
<PopoverTrigger
asChild
className="hover:bg-background-tint-03 rounded-08"
>
{triggerContent}
</PopoverTrigger>
<PopoverContent
side="top"
align={align || "end"}
className="w-64 p-1 bg-background border border-background-200 rounded-md shadow-lg flex flex-col"
className="w-64 p-1 bg-background-tint-01 border shadow-lg flex flex-col"
>
<div className="flex-grow max-h-[300px] default-scrollbar overflow-y-auto">
{llmOptionsToChooseFrom.map(
@@ -139,12 +141,7 @@ export default function LLMPopover({
return (
<button
key={index}
className={`w-full flex items-center gap-x-2 px-3 py-2 text-sm text-left hover:bg-background-100 dark:hover:bg-neutral-800 transition-colors duration-150 ${
(currentModelName || llmManager.currentLlm.modelName) ===
modelName
? "bg-background-100 dark:bg-neutral-900 text-text"
: "text-text-darker"
}`}
className={`w-full flex items-center gap-x-2 px-3 py-2 text-sm text-left hover:bg-background-tint-03 text-text-04 ${(currentModelName || llmManager.currentLlm.modelName) === modelName && "bg-background-tint-02"}`}
onClick={() => {
llmManager.updateCurrentLlm({
modelName,
@@ -157,7 +154,7 @@ export default function LLMPopover({
>
{icon({
size: 16,
className: "flex-none my-auto text-black",
className: "flex-none my-auto",
})}
<TruncatedText text={getDisplayNameForModel(modelName)} />
{(() => {
@@ -182,7 +179,7 @@ export default function LLMPopover({
<Tooltip>
<TooltipTrigger className="my-auto flex items-center ml-auto">
<FiAlertTriangle
className="text-alert"
className="text-status-warning-05"
size={16}
/>
</TooltipTrigger>
@@ -204,7 +201,7 @@ export default function LLMPopover({
)}
</div>
{user?.preferences?.temperature_override_enabled && (
<div className="mt-2 pt-2 border-t border-background-200">
<div className="mt-2 pt-2 border-t border-border-01">
<div className="w-full px-3 py-2">
<Slider
value={[localTemperature]}
@@ -215,7 +212,7 @@ export default function LLMPopover({
onValueCommit={handleTemperatureChangeComplete}
className="w-full"
/>
<div className="flex justify-between text-xs text-text-500 mt-2">
<div className="flex justify-between text-xs mt-2">
<span>Temperature (creativity)</span>
<span>{localTemperature.toFixed(1)}</span>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,31 @@
import { StarterMessage } from "@/app/admin/assistants/interfaces";
import Text from "@/components-2/Text";
export interface StarterMessageProps {
starterMessages: StarterMessage[];
onSelectStarterMessage: (message: string) => void;
}
export function StarterMessageDisplay({
starterMessages,
onSelectStarterMessage,
}: {
starterMessages: StarterMessage[];
onSelectStarterMessage: (message: string) => void;
}) {
}: StarterMessageProps) {
return (
<div
data-testid="starter-messages"
className="flex flex-col gap-2 w-full max-w-searchbar-max mx-auto"
className="flex flex-col w-full max-w-[40rem] p-spacing-inline gap-spacing-inline"
>
{starterMessages.map((starterMessage, index) => (
<div
key={starterMessage.name}
{starterMessages.map(({ name, message }, index) => (
<button
key={index}
data-testid={`starter-message-${index}`}
onClick={() => onSelectStarterMessage(starterMessage.message)}
className="
text-left
text-text-500
text-sm
mx-7
px-2
py-2
hover:bg-background-100
dark:hover:bg-neutral-800
rounded-lg
cursor-pointer
overflow-hidden
text-ellipsis
whitespace-nowrap
"
className="cursor-pointer bg-transparent hover:bg-background-tint-02 rounded-08 overflow-hidden text-ellipsis whitespace-nowrap p-padding-button"
onClick={() => onSelectStarterMessage(message)}
>
{starterMessage.name}
</div>
<Text text03 className="text-left">
{name}
</Text>
</button>
))}
</div>
);

View File

@@ -2,6 +2,10 @@ 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 AppSidebar from "@/sections/sidebar/AppSidebar";
import { fetchAppSidebarMetadata } from "@/lib/appSidebarSS";
import { getCurrentUserSS } from "@/lib/userSS";
import { AppSidebarProvider } from "@/components-2/context/AppSidebarContext";
export default async function Layout({
children,
@@ -13,9 +17,12 @@ export default async function Layout({
// Ensure searchParams is an object, even if it's empty
const safeSearchParams = {};
const data = await fetchChatData(
safeSearchParams as { [key: string]: string }
);
const [user, data] = await Promise.all([
getCurrentUserSS(),
fetchChatData(safeSearchParams),
]);
const { folded } = await fetchAppSidebarMetadata(user);
if ("redirect" in data) {
console.log("redirect", data.redirect);
@@ -25,7 +32,6 @@ export default async function Layout({
const {
chatSessions,
availableSources,
user,
documentSets,
tags,
llmProviders,
@@ -62,7 +68,21 @@ export default async function Layout({
defaultAssistantId,
}}
>
{children}
<AppSidebarProvider folded={folded}>
<div className="flex flex-row h-full w-full">
<AppSidebar />
<div className="flex flex-row h-full w-full">
{/* Mode Selection Section */}
<div className="flex-1" />
{/* Main Section */}
<div className="h-full w-[60%]">{children}</div>
{/* Side Section */}
<div className="flex-1" />
</div>
</div>
</AppSidebarProvider>
</ChatProvider>
</>
);

View File

@@ -5,8 +5,8 @@ import React, { useEffect, useRef, useState } from "react";
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
import { ChatFileType, FileDescriptor } from "@/app/chat/interfaces";
import { Hoverable, HoverableIcon } from "@/components/Hoverable";
import { DocumentPreview } from "../components/files/documents/DocumentPreview";
import { InMessageImage } from "../components/files/images/InMessageImage";
import { DocumentPreview } from "@/app/chat/components/files/documents/DocumentPreview";
import { InMessageImage } from "@/app/chat/components/files/images/InMessageImage";
import "prismjs/themes/prism-tomorrow.css";
import "./custom-code-styles.css";
import {
@@ -15,20 +15,23 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import ToolResult from "../../../components/tools/ToolResult";
import CsvContent from "../../../components/tools/CSVContent";
import ToolResult from "@/components/tools/ToolResult";
import CsvContent from "@/components/tools/CSVContent";
import "katex/dist/katex.min.css";
import { MessageSwitcher } from "./MessageSwitcher";
import { MessageSwitcher } from "@/app/chat/message/MessageSwitcher";
import Button from "@/components-2/buttons/Button";
interface FileDisplayProps {
files: FileDescriptor[];
alignBubble?: boolean;
setPresentingDocument: (document: MinimalOnyxDocument) => void;
}
function FileDisplay({
files,
alignBubble,
setPresentingDocument,
}: {
files: FileDescriptor[];
alignBubble?: boolean;
setPresentingDocument: (document: MinimalOnyxDocument) => void;
}) {
}: FileDisplayProps) {
const [close, setClose] = useState(true);
const imageFiles = files.filter((file) => file.type === ChatFileType.IMAGE);
const textFiles = files.filter(
@@ -103,18 +106,7 @@ function FileDisplay({
);
}
export const HumanMessage = ({
content,
files,
messageId,
otherMessagesCanSwitchTo,
onEdit,
onMessageSelection,
shared,
stopGenerating = () => null,
disableSwitchingForStreaming = false,
setPresentingDocument,
}: {
interface HumanMessageProps {
shared?: boolean;
content: string;
files?: FileDescriptor[];
@@ -125,19 +117,84 @@ export const HumanMessage = ({
stopGenerating?: () => void;
disableSwitchingForStreaming?: boolean;
setPresentingDocument: (document: MinimalOnyxDocument) => void;
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
}
interface EditingAreaProps {
textareaRef: React.RefObject<HTMLTextAreaElement>;
editedContent: string;
setEditedContent: (value: string) => void;
onSubmit: () => void;
onCancel: () => void;
originalContent: string;
}
function EditingArea({
textareaRef,
editedContent,
setEditedContent,
onSubmit,
onCancel,
originalContent,
}: EditingAreaProps) {
return (
<div className="w-full flex flex-col border rounded-08 p-spacing-interline bg-background-tint-02">
<textarea
ref={textareaRef}
className="w-full h-full overflow-y-hidden whitespace-normal break-word overscroll-contain resize-none overflow-y-auto bg-background-tint-02 outline-none p-padding-button"
aria-multiline
role="textarea"
value={editedContent}
style={{ scrollbarWidth: "thin" }}
onChange={(event) => {
setEditedContent(event.target.value);
textareaRef.current!.style.height = "auto";
event.target.style.height = `${event.target.scrollHeight}px`;
}}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
setEditedContent(originalContent);
onCancel();
}
// Submit edit if "Command Enter" is pressed, like in ChatGPT
else if (event.key === "Enter" && event.metaKey) {
onSubmit();
}
}}
/>
<div className="flex flex-row justify-end gap-spacing-inline">
<Button onClick={onSubmit}>Submit</Button>
<Button
secondary
onClick={() => {
setEditedContent(originalContent);
onCancel();
}}
>
Cancel
</Button>
</div>
</div>
);
}
export function HumanMessage({
content,
files,
messageId,
otherMessagesCanSwitchTo,
onEdit,
onMessageSelection,
shared,
stopGenerating = () => null,
disableSwitchingForStreaming = false,
setPresentingDocument,
}: HumanMessageProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [editedContent, setEditedContent] = useState(content);
useEffect(() => {
if (!isEditing) {
setEditedContent(content);
}
}, [content, isEditing]);
useEffect(() => setEditedContent(content), [content]);
useEffect(() => {
if (textareaRef.current) {
// Focus the textarea
@@ -187,11 +244,7 @@ export const HumanMessage = ({
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={`text-user-text mx-auto ${
shared ? "w-full" : "w-[90%]"
} max-w-[790px]`}
>
<div className={`mx-auto ${shared ? "w-full" : "w-[90%]"} max-w-[790px]`}>
<div className="xl:ml-8">
<div className="flex flex-col desktop:mr-4">
<FileDisplay
@@ -203,112 +256,14 @@ export const HumanMessage = ({
<div className="flex justify-end">
<div className="w-full ml-8 flex w-full w-[800px] break-words">
{isEditing ? (
<div className="w-full">
<div
className={`
opacity-100
w-full
flex
flex-col
border
border-border
rounded-lg
pb-2
[&:has(textarea:focus)]::ring-1
[&:has(textarea:focus)]::ring-black
`}
>
<textarea
ref={textareaRef}
className={`
m-0
w-full
h-auto
shrink
border-0
rounded-lg
overflow-y-hidden
whitespace-normal
break-word
overscroll-contain
outline-none
placeholder-text-400
resize-none
text-text-editing-message
pl-4
overflow-y-auto
bg-background
pr-12
py-4`}
aria-multiline
role="textarea"
value={editedContent}
style={{ scrollbarWidth: "thin" }}
onChange={(e) => {
setEditedContent(e.target.value);
textareaRef.current!.style.height = "auto";
e.target.style.height = `${e.target.scrollHeight}px`;
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
setEditedContent(content);
setIsEditing(false);
}
// Submit edit if "Command Enter" is pressed, like in ChatGPT
if (e.key === "Enter" && e.metaKey) {
handleEditSubmit();
}
}}
/>
<div className="flex justify-end mt-2 gap-2 pr-4">
<button
className={`
w-fit
bg-agent
text-inverted
text-sm
rounded-lg
inline-flex
items-center
justify-center
flex-shrink-0
font-medium
min-h-[38px]
py-2
px-3
hover:bg-agent-hovered
`}
onClick={handleEditSubmit}
>
Submit
</button>
<button
className={`
inline-flex
items-center
justify-center
flex-shrink-0
font-medium
min-h-[38px]
py-2
px-3
w-fit
bg-background-200
text-sm
rounded-lg
hover:bg-accent-background-hovered-emphasis
`}
onClick={() => {
setEditedContent(content);
setIsEditing(false);
}}
>
Cancel
</button>
</div>
</div>
</div>
<EditingArea
textareaRef={textareaRef}
editedContent={editedContent}
setEditedContent={setEditedContent}
onSubmit={handleEditSubmit}
onCancel={() => setIsEditing(false)}
originalContent={content}
/>
) : typeof content === "string" ? (
<>
<div className="ml-auto flex items-center mr-1 mt-2 h-fit mb-auto">
@@ -320,7 +275,7 @@ export const HumanMessage = ({
<Tooltip>
<TooltipTrigger>
<HoverableIcon
icon={<FiEdit2 className="text-text-600" />}
icon={<FiEdit2 className="text-text-05" />}
onClick={() => {
setIsEditing(true);
setIsHovered(false);
@@ -343,9 +298,9 @@ export const HumanMessage = ({
!isEditing &&
(!files || files.length === 0)
) && "ml-auto"
} relative text-text flex-none max-w-[70%] mb-auto whitespace-break-spaces rounded-3xl bg-user px-5 py-2.5`}
} relative flex-none max-w-[70%] mb-auto whitespace-break-spaces rounded-bl-3xl rounded-t-3xl bg-background-neutral-02 px-5 py-2.5`}
>
{content}
{editedContent}
</div>
</>
) : (
@@ -366,7 +321,9 @@ export const HumanMessage = ({
) : (
<div className="h-[27px]" />
)}
<div className="ml-auto rounded-lg p-1">{content}</div>
<div className="ml-auto rounded-lg p-1">
{editedContent}
</div>
</>
)}
</div>
@@ -405,4 +362,4 @@ export const HumanMessage = ({
</div>
</div>
);
};
}

View File

@@ -4,15 +4,15 @@ import {
CitationDelta,
SearchToolDelta,
StreamingCitation,
} from "../../services/streamingModels";
import { FullChatState } from "./interfaces";
} from "@/app/chat/services/streamingModels";
import { FullChatState } from "@/app/chat/message/messageComponents/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Logo } from "@/components/logo/Logo";
import { CopyButton } from "@/components/CopyButton";
import { LikeFeedback, DislikeFeedback } from "@/components/icons/icons";
import { HoverableIcon } from "@/components/Hoverable";
import { OnyxDocument } from "@/lib/search/interfaces";
import { CitedSourcesToggle } from "./CitedSourcesToggle";
import { CitedSourcesToggle } from "@/app/chat/message/messageComponents/CitedSourcesToggle";
import {
CustomTooltip,
TooltipGroup,
@@ -22,21 +22,29 @@ import {
useChatSessionStore,
useDocumentSidebarVisible,
useSelectedNodeForDocDisplay,
} from "../../stores/useChatSessionStore";
import { copyAll, handleCopy } from "../copyingUtils";
import RegenerateOption from "../../components/RegenerateOption";
import { MessageSwitcher } from "../MessageSwitcher";
import { BlinkingDot } from "../BlinkingDot";
} from "@/app/chat/stores/useChatSessionStore";
import { copyAll, handleCopy } from "@/app/chat/message/copyingUtils";
import RegenerateOption from "@/app/chat/components/RegenerateOption";
import { MessageSwitcher } from "@/app/chat/message/MessageSwitcher";
import { BlinkingDot } from "@/app/chat/message/BlinkingDot";
import {
getTextContent,
isDisplayPacket,
isFinalAnswerComing,
isStreamingComplete,
isToolPacket,
} from "../../services/packetUtils";
import { useMessageSwitching } from "./hooks/useMessageSwitching";
import MultiToolRenderer from "./MultiToolRenderer";
import { RendererComponent } from "./renderMessageComponent";
} from "@/app/chat/services/packetUtils";
import { useMessageSwitching } from "@/app/chat/message/messageComponents/hooks/useMessageSwitching";
import MultiToolRenderer from "@/app/chat/message/messageComponents/MultiToolRenderer";
import { RendererComponent } from "@/app/chat/message/messageComponents/renderMessageComponent";
interface AIMessageProps {
rawPackets: Packet[];
chatState: FullChatState;
nodeId: number;
otherMessagesCanSwitchTo?: number[];
onMessageSelection?: (nodeId: number) => void;
}
export function AIMessage({
rawPackets,
@@ -44,13 +52,7 @@ export function AIMessage({
nodeId,
otherMessagesCanSwitchTo,
onMessageSelection,
}: {
rawPackets: Packet[];
chatState: FullChatState;
nodeId: number;
otherMessagesCanSwitchTo?: number[];
onMessageSelection?: (nodeId: number) => void;
}) {
}: AIMessageProps) {
const markdownRef = useRef<HTMLDivElement>(null);
const [isRegenerateDropdownVisible, setIsRegenerateDropdownVisible] =
useState(false);
@@ -417,7 +419,7 @@ export function AIMessage({
documentMap.size > 0) && (
<>
{chatState.regenerate && (
<div className="h-4 w-px bg-border mx-2" />
<div className="h-4 w-px mx-2" />
)}
<CustomTooltip
showTick

View File

@@ -76,7 +76,7 @@ interface FileUploadSectionProps {
onUploadProgress?: (fileName: string, progress: number) => void;
}
export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
export function FileUploadSection({
onUpload,
onUrlUpload,
disabledMessage,
@@ -84,7 +84,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
isUploading = false,
onUploadComplete,
onUploadProgress,
}) => {
}: FileUploadSectionProps) {
const [uploadType, setUploadType] = useState<"file" | "url">("file");
const [fileUrl, setFileUrl] = useState("");
const [urlError, setUrlError] = useState<string | null>(null);
@@ -431,7 +431,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
<div className="mt-4 max-w-xl w-full">
{/* Invalid file message */}
{showInvalidFileMessage && invalidFiles.length > 0 && (
<div className="mb-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md text-yellow-800 dark:text-yellow-200 text-sm flex items-start">
<div className="mb-4 p-3 bg-status-warning-00 border border-status-warning-02 rounded-md text-status-warning-05 text-sm flex items-start">
<AlertCircle className="w-5 h-5 mr-2 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-medium">
@@ -451,7 +451,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
</div>
<button
onClick={() => setShowInvalidFileMessage(false)}
className="flex-shrink-0 text-yellow-700 dark:text-yellow-300 hover:text-yellow-900 dark:hover:text-yellow-100"
className="flex-shrink-0 text-status-warning-05 hover:text-status-warning-05"
>
<X className="w-4 h-4" />
</button>
@@ -467,10 +467,9 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
className={`w-full ${uploadType === "url" ? "cursor-default" : ""}`}
>
<div
className={`border bg-transparent border-neutral-200 dark:border-neutral-700 bg- rounded-lg shadow-sm
className={`border bg-transparent rounded-lg shadow-sm
${
uploadType === "file" &&
"hover:bg-neutral-50 dark:hover:bg-neutral-800"
uploadType === "file" && "hover:bg-background-tint-02"
} transition-colors duration-200
${uploadType === "file" ? "cursor-pointer" : "cursor-default"}
h-[160px] flex items-center justify-center`}
@@ -483,7 +482,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
htmlFor="file-upload"
className={`w-full p-4 h-full cursor-pointer flex flex-col items-center justify-center ${
isDragging
? "border-2 border-dashed border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/20 rounded-md"
? "border-2 border-dashed border-action-link-05 bg-action-link-00 rounded-md"
: ""
} transition-all duration-150 ease-in-out`}
onDragEnter={handleDragEnter}
@@ -495,14 +494,12 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
<div className="h-[40px] flex items-center justify-center">
<Upload
className={`w-6 h-6 ${
isDragging
? "text-blue-500 dark:text-blue-400"
: "text-neutral-400 dark:text-neutral-500"
isDragging ? "text-action-link-05" : "text-text-03"
}`}
/>
</div>
<div className="mt-2">
<p className="text-center text-sm text-neutral-500 dark:text-neutral-400">
<p className="text-center text-sm">
{isDragging
? "Drop files here..."
: "Drag & drop or click to upload files"}
@@ -520,7 +517,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
<>
{/* Icon container - fixed position for both modes */}
<div className="h-[40px] flex items-center justify-center mt-6">
<Link className="w-6 h-6 text-neutral-400 dark:text-neutral-500" />
<Link className="w-6 h-6" />
</div>
{/* Content area - different for each mode but with consistent spacing */}
@@ -533,8 +530,8 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
className={`w-full text-sm py-2 px-3 border rounded-md bg-transparent focus:outline-none focus:ring-1
${
urlError
? "border-red-400 dark:border-red-600 focus:ring-red-400 dark:focus:ring-red-600"
: "border-neutral-200 dark:border-neutral-700 focus:ring-neutral-300 dark:focus:ring-neutral-600"
? "border-status-error-05 focus:ring-status-error-05"
: "border focus:ring-border-03"
}`}
value={fileUrl}
onChange={handleUrlChange}
@@ -546,8 +543,8 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
disabled={!fileUrl || isProcessing}
className={`p-2 rounded-md ${
!fileUrl || isProcessing
? "text-neutral-400 cursor-not-allowed"
: "text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700"
? "text-text-03 cursor-not-allowed"
: "text-text-01 hover:bg-background-tint-02"
}`}
>
{isProcessing ? (
@@ -558,7 +555,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
</button>
</div>
{urlError && (
<p className="text-red-500 dark:text-red-400 text-xs mt-1 max-w-md px-1">
<p className="text-status-error-05 text-xs mt-1 max-w-md px-1">
{urlError}
</p>
)}
@@ -570,14 +567,10 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
<div className="flex bg-neutral-100 dark:bg-neutral-800 p-1 rounded-lg self-center mt-2 w-fit mx-auto">
<div className="flex bg-background-tint-01 p-1 rounded-lg self-center mt-2 w-fit mx-auto">
<button
type="button"
className={`px-3 py-1.5 rounded-md flex items-center justify-center gap-1.5 text-xs transition-all ${
uploadType === "file"
? "bg-white dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200 shadow-sm font-medium"
: "text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700"
}`}
className={`px-3 py-1.5 rounded-md flex items-center justify-center gap-1.5 text-xs transition-all hover:bg-background-tint-03 ${uploadType === "file" && "bg-background-tint-02 shadow-md"}`}
onClick={() => setUploadType("file")}
>
<Upload className="w-3.5 h-3.5" />
@@ -585,11 +578,7 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
</button>
<button
type="button"
className={`px-3 py-1.5 rounded-md flex items-center justify-center gap-1.5 text-xs transition-all ${
uploadType === "url"
? "bg-white dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200 shadow-sm font-medium"
: "text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700"
}`}
className={`px-3 py-1.5 rounded-md flex items-center justify-center gap-1.5 text-xs transition-all hover:bg-background-tint-03 ${uploadType === "url" && "bg-background-tint-02 shadow-md"}`}
onClick={() => setUploadType("url")}
>
<Link className="w-3.5 h-3.5" />
@@ -598,4 +587,4 @@ export const FileUploadSection: React.FC<FileUploadSectionProps> = ({
</div>
</div>
);
};
}

View File

@@ -12,7 +12,7 @@ import {
FileStatus,
FolderResponse,
useDocumentsContext,
} from "../DocumentsContext";
} from "@/app/chat/my-documents/DocumentsContext";
import {
Tooltip,
TooltipContent,
@@ -29,7 +29,7 @@ import {
} from "react-icons/fi";
import { getFormattedDateTime } from "@/lib/dateUtils";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
import { AnimatedDots } from "../[id]/components/DocumentList";
import { AnimatedDots } from "@/app/chat/my-documents/[id]/components/DocumentList";
import { FolderMoveIcon } from "@/components/icons/icons";
import { truncateString } from "@/lib/utils";
import { usePopup } from "@/components/admin/connectors/Popup";
@@ -52,7 +52,7 @@ interface FileListItemProps {
status: FileStatus;
}
export const FileListItem: React.FC<FileListItemProps> = ({
export function FileListItem({
file,
isSelected,
onSelect,
@@ -62,7 +62,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
onMove,
folders,
status,
}) => {
}: FileListItemProps) {
const { setPopup, popup } = usePopup();
const [showMoveOptions, setShowMoveOptions] = useState(false);
const [indexingStatus, setIndexingStatus] = useState<boolean | null>(null);
@@ -108,7 +108,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
<FiAlertTriangle className="h-4 w-4" />
</div>
</PopoverTrigger>
<PopoverContent className="w-56 p-3 shadow-lg rounded-md border border-neutral-200 dark:border-neutral-800">
<PopoverContent className="w-56 p-3 shadow-lg rounded-md border">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<p className="text-xs font-medium text-red-500">
@@ -122,7 +122,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
<Button
variant="outline"
size="sm"
className="w-full justify-start text-sm font-medium hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
className="w-full justify-start text-sm font-medium hover:bg-background-tint-02 transition-colors"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -160,7 +160,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
<Button
variant="outline"
size="sm"
className="w-full justify-start text-sm font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 transition-colors"
className="w-full justify-start text-sm font-medium text-status-error-05 hover:bg-status-error-00 hover:text-status-error-05 transition-colors"
onClick={(e) => {
e.stopPropagation();
setIsPopoverOpen(false);
@@ -187,7 +187,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
return (
<div
className="group relative flex cursor-pointer items-center border-b border-border dark:border-border-200 hover:bg-[#f2f0e8]/50 dark:hover:bg-[#1a1a1a]/50 py-3 px-4 transition-all ease-in-out"
className="group relative flex cursor-pointer items-center border-b border hover:bg-background-tint-02 py-3 px-4 transition-all ease-in-out"
onClick={(e) => {
if (!(e.target as HTMLElement).closest(".action-menu")) {
onSelect && onSelect(file);
@@ -208,7 +208,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate text-sm text-text-dark dark:text-text-dark">
<span className="truncate text-sm">
{truncateString(file.name, 50)}
</span>
</TooltipTrigger>
@@ -218,20 +218,18 @@ export const FileListItem: React.FC<FileListItemProps> = ({
</Tooltip>
</TooltipProvider>
) : (
<span className="truncate text-sm text-text-dark dark:text-text-dark">
{file.name}
</span>
<span className="truncate text-sm">{file.name}</span>
)}
</div>
<div className="w-[30%] text-sm text-text-400 dark:text-neutral-400">
<div className="w-[30%] text-sm">
{file.created_at &&
getFormattedDateTime(
new Date(new Date(file.created_at).getTime() - 8 * 60 * 60 * 1000)
)}
</div>
<div className="w-[30%] text-sm text-text-400 dark:text-neutral-400">
<div className="w-[30%] text-sm">
{file.status == FileStatus.INDEXING ||
file.status == FileStatus.REINDEXING ? (
<>
@@ -294,7 +292,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
</Button>
</div>
) : (
<div className="p-2 text-text-dark space-y-2">
<div className="p-2 space-y-2">
<div className="flex items-center space-x-2 mb-4">
<h3 className="text-sm px-2 font-semibold">Move to </h3>
</div>
@@ -319,7 +317,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
(folder) =>
folder.id !== -1 && folder.id !== file.folder_id
).length === 0 && (
<div className="text-sm text-gray-500 px-2 text-center">
<div className="text-sm px-2 text-center">
No folders available to move this file to.
</div>
)}
@@ -332,23 +330,4 @@ export const FileListItem: React.FC<FileListItemProps> = ({
</div>
</div>
);
};
export const SkeletonFileListItem: React.FC<{ view: "grid" | "list" }> = () => {
return (
<div className="group relative flex items-center border-b border-border dark:border-border-200 py-3 px-4">
<div className="flex items-center flex-1 min-w-0">
<div className="flex items-center gap-3 w-[40%]">
<div className="h-5 w-5 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
<div className="h-4 w-48 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
</div>
<div className="w-[30%]">
<div className="h-4 w-24 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
</div>
<div className="w-[30%]">
<div className="h-4 w-24 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
</div>
</div>
</div>
);
};
}

View File

@@ -2,12 +2,12 @@ import React, { useState, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Modal } from "@/components/Modal";
import { FolderIcon, ArrowUp, ArrowDown } from "lucide-react";
import { SelectedItemsList } from "./SelectedItemsList";
import { SelectedItemsList } from "@/app/chat/my-documents/components/SelectedItemsList";
import {
useDocumentsContext,
FolderResponse,
FileResponse,
} from "../DocumentsContext";
} from "@/app/chat/my-documents/DocumentsContext";
import {
DndContext,
closestCenter,
@@ -36,7 +36,7 @@ import {
} from "@/components/ui/tooltip";
import { usePopup } from "@/components/admin/connectors/Popup";
import { getFormattedDateTime } from "@/lib/dateUtils";
import { FileUploadSection } from "../[id]/components/upload/FileUploadSection";
import { FileUploadSection } from "@/app/chat/my-documents/[id]/components/upload/FileUploadSection";
import { truncateString } from "@/lib/utils";
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
@@ -48,14 +48,23 @@ export interface UploadingFile {
progress: number;
}
const DraggableItem: React.FC<{
interface DraggableItemProps {
id: string;
type: "folder" | "file";
item: FolderResponse | FileResponse;
onClick?: () => void;
onSelect?: (e: React.MouseEvent<HTMLDivElement>) => void;
isSelected: boolean;
}> = ({ id, type, item, onClick, onSelect, isSelected }) => {
}
function DraggableItem({
id,
type,
item,
onClick,
onSelect,
isSelected,
}: DraggableItemProps) {
const {
attributes,
listeners,
@@ -74,8 +83,8 @@ const DraggableItem: React.FC<{
};
const selectedClassName = isSelected
? "bg-neutral-200/50 dark:bg-neutral-800/50"
: "hover:bg-neutral-200/50 dark:hover:bg-neutral-800/50";
? "bg-background-tint-02"
: "hover:bg-background-tint-02";
if (type === "folder") {
return (
@@ -112,11 +121,7 @@ const DraggableItem: React.FC<{
}}
>
<div
className={`w-4 h-4 border rounded ${
isSelected
? "bg-black border-black"
: "border-neutral-400 hover:bg-neutral-100 dark:border-neutral-600"
} flex items-center justify-center cursor-pointer hover:border-neutral-500 dark:hover:border-neutral-500 dark:hover:bg-neutral-800`}
className={`w-4 h-4 border rounded hover:bg-background-tint-01 flex items-center justify-center cursor-pointer`}
>
{isSelected && (
<svg
@@ -128,7 +133,7 @@ const DraggableItem: React.FC<{
>
<path
d="M20 6L9 17L4 12"
stroke="white"
stroke="var(--text-05)"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
@@ -139,7 +144,7 @@ const DraggableItem: React.FC<{
</div>
</div>
<div
className={`group w-full relative flex cursor-pointer items-center border-b border-border dark:border-border-200 ${selectedClassName} py-2 px-3 transition-all ease-in-out`}
className={`group w-full relative flex cursor-pointer items-center border-b ${selectedClassName} py-2 px-3 transition-all ease-in-out`}
>
<div className="flex items-center flex-1 min-w-0" onClick={onClick}>
<div className="flex text-sm items-center gap-2 w-[65%] min-w-0">
@@ -148,7 +153,7 @@ const DraggableItem: React.FC<{
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate text-text-dark dark:text-text-dark">
<span className="truncate">
{truncateString(file.name, 34)}
</span>
</TooltipTrigger>
@@ -158,13 +163,11 @@ const DraggableItem: React.FC<{
</Tooltip>
</TooltipProvider>
) : (
<span className="truncate text-text-dark dark:text-text-dark">
{file.name}
</span>
<span className="truncate">{file.name}</span>
)}
</div>
<div className="w-[35%] text-right text-sm text-text-400 dark:text-neutral-400 pr-4">
<div className="w-[35%] text-right text-sm pr-4">
{file.created_at
? getFormattedDateTime(new Date(file.created_at))
: ""}
@@ -173,19 +176,27 @@ const DraggableItem: React.FC<{
</div>
</div>
);
};
}
const FilePickerFolderItem: React.FC<{
interface FilePickerFolderItemProps {
folder: FolderResponse;
onClick: () => void;
onSelect: (e: React.MouseEvent<HTMLDivElement>) => void;
isSelected: boolean;
allFilesSelected: boolean;
}> = ({ folder, onClick, onSelect, isSelected, allFilesSelected }) => {
}
function FilePickerFolderItem({
folder,
onClick,
onSelect,
isSelected,
allFilesSelected,
}: FilePickerFolderItemProps) {
const selectedClassName =
isSelected || allFilesSelected
? "bg-neutral-200/50 dark:bg-neutral-800/50"
: "hover:bg-neutral-200/50 dark:hover:bg-neutral-800/50";
? "bg-background-tint-02"
: "hover:bg-background-tint-02";
// Determine if the folder is empty
const isEmpty = folder.files.length === 0;
@@ -207,11 +218,7 @@ const FilePickerFolderItem: React.FC<{
}}
>
<div
className={`w-4 h-4 border rounded ${
isSelected || allFilesSelected
? "bg-black border-black"
: "border-neutral-400 dark:border-neutral-600"
} flex items-center justify-center cursor-pointer hover:border-neutral-500 dark:hover:border-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-800`}
className={`w-4 h-4 border rounded flex items-center justify-center cursor-pointer hover:bg-background-tint-01`}
>
{(isSelected || allFilesSelected) && (
<svg
@@ -223,7 +230,7 @@ const FilePickerFolderItem: React.FC<{
>
<path
d="M20 6L9 17L4 12"
stroke="white"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
@@ -235,19 +242,19 @@ const FilePickerFolderItem: React.FC<{
)}
</div>
<div
className={`group w-full relative flex cursor-pointer items-center border-b border-border dark:border-border-200 ${
className={`group w-full relative flex cursor-pointer items-center border-b ${
!isEmpty ? selectedClassName : ""
} py-2 px-3 transition-all ease-in-out`}
>
<div className="flex items-center flex-1 min-w-0" onClick={onClick}>
<div className="flex text-sm items-center gap-2 w-[65%] min-w-0">
<FolderIcon className="h-5 w-5 text-black dark:text-black shrink-0 fill-black dark:fill-black" />
<FolderIcon className="h-5 w-5 shrink-0" />
{folder.name.length > 40 ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate text-text-dark dark:text-text-dark">
<span className="truncate">
{truncateString(folder.name, 40)}
</span>
</TooltipTrigger>
@@ -257,20 +264,18 @@ const FilePickerFolderItem: React.FC<{
</Tooltip>
</TooltipProvider>
) : (
<span className="truncate text-text-dark dark:text-text-dark">
{folder.name}
</span>
<span className="truncate">{folder.name}</span>
)}
</div>
<div className="w-[35%] text-right text-sm text-text-400 dark:text-neutral-400 pr-4">
<div className="w-[35%] text-right text-sm text-text-04 pr-4">
{folder.files.length} {folder.files.length === 1 ? "file" : "files"}
</div>
</div>
</div>
</div>
);
};
}
export interface FilePickerModalProps {
isOpen: boolean;
@@ -300,13 +305,13 @@ enum SortDirection {
Descending = "desc",
}
export const FilePickerModal: React.FC<FilePickerModalProps> = ({
export function FilePickerModal({
isOpen,
onClose,
onSave,
setPresentingDocument,
buttonContent,
}) => {
}: FilePickerModalProps) {
const {
folders,
refreshFolders,
@@ -927,7 +932,7 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
if (currentFolder !== null) {
return (
<div
className="flex items-center mb-2 text-sm text-neutral-600 cursor-pointer hover:text-neutral-800"
className="flex items-center mb-2 text-sm cursor-pointer"
onClick={() => setCurrentFolder(null)}
>
<svg
@@ -1098,21 +1103,21 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
}
>
<div className="h-[calc(70vh-5rem)] flex overflow-visible flex-col">
<div className="grid overflow-x-visible h-full overflow-y-hidden flex-1 w-full divide-x divide-neutral-200 dark:divide-neutral-700 desktop:grid-cols-2">
<div className="w-full h-full pb-4 overflow-hidden ">
<div className="grid overflow-x-visible h-full overflow-y-hidden flex-1 w-full divide-x desktop:grid-cols-2">
<div className="w-full h-full pb-4 overflow-hidden">
<div className="px-6 sticky flex flex-col gap-y-2 z-[1000] top-0 mb-2 flex gap-x-2 w-full pr-4">
<div className="w-full relative">
<input
type="text"
placeholder="Search documents..."
className="w-full pl-10 pr-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md focus:border-transparent dark:bg-neutral-800 dark:text-neutral-100"
className="w-full pl-10 pr-4 py-2 border rounded-md focus:border-transparent bg-background-tint-00"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-text-dark dark:text-neutral-400"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -1130,8 +1135,8 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
</div>
{filteredFolders.length + currentFolderFiles.length > 0 ? (
<div className="pl-2 h-full flex-grow overflow-y-auto max-h-full default-scrollbar pr-4">
<div className="flex ml-6 items-center border-b border-border dark:border-border-200 py-2 pr-3 text-sm font-medium text-text-400 dark:text-neutral-400">
<div className="pl-2 h-full flex-grow overflow-y-auto max-h-full default-scrollbar pr-4">
<div className="flex ml-6 items-center border-b py-2 pr-3 text-sm font-medium">
<div className="flex pl-2 items-center gap-3 w-[65%] min-w-0">
<button
onClick={() => handleSortChange(SortType.Alphabetical)}
@@ -1255,18 +1260,14 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
</div>
) : folders.length > 0 ? (
<div className="flex-grow overflow-y-auto px-4">
<p className="text-text-subtle dark:text-neutral-400">
No folders found
</p>
<p>No folders found</p>
</div>
) : (
<div className="flex-grow flex-col overflow-y-auto px-4 flex items-start justify-start gap-y-2">
<p className="text-sm text-muted-foreground dark:text-neutral-400">
No folders found
</p>
<p className="text-sm">No folders found</p>
<a
href="/chat/my-documents?createFolder=true"
className="inline-flex items-center text-sm justify-center text-neutral-600 dark:text-neutral-400 hover:underline"
className="inline-flex items-center text-sm justify-center hover:underline"
>
<FolderIcon className="mr-2 h-4 w-4" />
Create folder in My Documents
@@ -1276,7 +1277,7 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
</div>
<div
className={`mobile:hidden overflow-y-auto w-full h-full flex flex-col ${
isHoveringRight ? "bg-neutral-100 dark:bg-neutral-800/30" : ""
isHoveringRight ? "bg-background-tint-02" : ""
}`}
onDragEnter={() => setIsHoveringRight(true)}
onDragLeave={() => setIsHoveringRight(false)}
@@ -1284,9 +1285,7 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
<div className="px-5 h-full flex flex-col">
{/* Top section: scrollable, takes remaining space */}
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">
Selected Items
</h3>
<h3 className="text-sm font-semibold">Selected Items</h3>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<SelectedItemsList
@@ -1367,12 +1366,10 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
</div>
</div>
</div>
<div className="px-5 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<div className="px-5 pt-4 border-t">
<div className="flex flex-col items-center justify-center py-2 space-y-4">
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-600 dark:text-neutral-400">
Selected context:
</span>
<span className="text-sm">Selected context:</span>
<TokenDisplay
totalTokens={selectedItems.totalTokens}
maxTokens={selectedModel.maxTokens}
@@ -1416,4 +1413,4 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
</div>
</Modal>
);
};
}

View File

@@ -2,11 +2,14 @@ import React from "react";
import { cn, truncateString } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { X, FolderIcon, Loader2 } from "lucide-react";
import { FolderResponse, FileResponse } from "../DocumentsContext";
import {
FolderResponse,
FileResponse,
} from "@/app/chat/my-documents/DocumentsContext";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
import { UploadingFile } from "./FilePicker";
import { CircularProgress } from "../[id]/components/upload/CircularProgress";
import { UploadingFile } from "@/app/chat/my-documents/components/FilePicker";
import { CircularProgress } from "@/app/chat/my-documents/[id]/components/upload/CircularProgress";
interface SelectedItemsListProps {
folders: FolderResponse[];
@@ -17,14 +20,14 @@ interface SelectedItemsListProps {
setPresentingDocument: (onyxDocument: MinimalOnyxDocument) => void;
}
export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
export function SelectedItemsList({
folders,
files,
uploadingFiles,
onRemoveFile,
onRemoveFolder,
setPresentingDocument,
}) => {
}: SelectedItemsListProps) {
const hasItems =
folders.length > 0 || files.length > 0 || uploadingFiles.length > 0;
const openFile = (file: FileResponse) => {
@@ -41,6 +44,7 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
return (
<div className="h-full w-full flex flex-col">
<div className="space-y-2.5 pb-2">
{/* Folders */}
{folders.length > 0 && (
<div className="space-y-2.5">
{folders.map((folder: FolderResponse) => (
@@ -48,17 +52,14 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-100/80 border-neutral-200 hover:bg-neutral-200/60",
"dark:bg-neutral-800/80 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"bg-background-tint-01 border hover:bg-background-tint-02",
"transition-colors duration-150"
)}
>
<div className="flex items-center min-w-0 flex-1">
<FolderIcon className="h-5 w-5 mr-2 text-black dark:text-black shrink-0 fill-black dark:fill-black" />
<FolderIcon className="h-5 w-5 mr-2 shrink-0" />
<span className="text-sm font-medium truncate text-neutral-800 dark:text-neutral-100">
<span className="text-sm font-medium truncate">
{truncateString(folder.name, 34)}
</span>
</div>
@@ -71,21 +72,19 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"hover:text-text-01",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove folder ${folder.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
<X className="h-3 w-3 " />
</Button>
</div>
))}
</div>
)}
{/* Files */}
{files.length > 0 && (
<div className="space-y-2.5 ">
{files.map((file: FileResponse) => (
@@ -96,10 +95,7 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"bg-background-tint-01 border hover:bg-background-tint-02",
"transition-colors duration-150",
"cursor-pointer"
)}
@@ -107,7 +103,7 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
>
<div className="flex items-center min-w-0 flex-1">
{getFileIconFromFileNameAndLink(file.name, file.link_url)}
<span className="text-sm truncate text-neutral-700 dark:text-neutral-200 ml-2.5">
<span className="text-sm truncate ml-2.5">
{truncateString(file.name, 34)}
</span>
</div>
@@ -119,20 +115,18 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"hover:text-text-01",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
<X className="h-3 w-3 " />
</Button>
</div>
))}
</div>
)}
<div className="max-w-full space-y-2.5">
{uploadingFiles
.filter(
@@ -145,10 +139,7 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
key={`uploading-${index}`}
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"bg-background-tint-01 border hover:bg-background-tint-02",
"transition-colors duration-150",
"cursor-pointer"
)}
@@ -164,7 +155,7 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
showPercentage={false}
/>
)}
<span className="truncate text-sm text-text-dark dark:text-text-dark">
<span className="truncate text-sm">
{uploadingFile.name.startsWith("http")
? `${uploadingFile.name.substring(0, 30)}${
uploadingFile.name.length > 30 ? "..." : ""
@@ -180,26 +171,23 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"hover:text-text-01",
"transition-all duration-150 ease-in-out"
)}
// aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
<X className="h-3 w-3 " />
</Button>
</div>
</div>
))}
</div>
{!hasItems && (
<div className="flex items-center justify-center h-24 text-sm text-neutral-500 dark:text-neutral-400 italic bg-neutral-50/50 dark:bg-neutral-800/30 rounded-md border border-neutral-200/50 dark:border-neutral-700/50">
<div className="flex items-center justify-center h-24 text-sm italic bg-background-tint-01 rounded-md border">
No items selected
</div>
)}
</div>
</div>
);
};
}

View File

@@ -1,21 +1,12 @@
import { DocumentsProvider } from "./my-documents/DocumentsContext";
import { SEARCH_PARAMS } from "@/lib/extension/constants";
import ChatLayout from "./WrappedChat";
import { ChatPage } from "@/app/chat/components/ChatPage";
export default async function Page(props: {
interface PageProps {
searchParams: Promise<{ [key: string]: string }>;
}) {
}
export default async function Page(props: PageProps) {
const searchParams = await props.searchParams;
const firstMessage = searchParams.firstMessage;
const defaultSidebarOff =
searchParams[SEARCH_PARAMS.DEFAULT_SIDEBAR_OFF] === "true";
return (
<DocumentsProvider>
<ChatLayout
firstMessage={firstMessage}
defaultSidebarOff={defaultSidebarOff}
/>
</DocumentsProvider>
);
return <ChatPage firstMessage={firstMessage} />;
}

View File

@@ -311,6 +311,7 @@ export async function handleChatFeedback(
});
return response;
}
export async function renameChatSession(
chatSessionId: string,
newName: string

View File

@@ -1,16 +0,0 @@
"use client";
import SidebarWrapper from "@/app/assistants/SidebarWrapper";
import { AssistantStats } from "./AssistantStats";
export default function WrappedAssistantsStats({
assistantId,
}: {
assistantId: number;
}) {
return (
<SidebarWrapper>
<AssistantStats assistantId={assistantId} />
</SidebarWrapper>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
import "./globals.css";
import "@/app/globals.css";
import {
fetchEnterpriseSettingsSS,
fetchSettingsSS,
@@ -9,6 +8,7 @@ import {
GTM_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
NEXT_PUBLIC_CLOUD_ENABLED,
MODAL_ROOT_ID,
} from "@/lib/constants";
import { Metadata } from "next";
import { buildClientUrl } from "@/lib/utilsSS";
@@ -16,21 +16,21 @@ import { Inter } from "next/font/google";
import {
EnterpriseSettings,
ApplicationStatus,
} from "./admin/settings/interfaces";
import { AppProvider } from "@/components/context/AppProvider";
import { PHProvider } from "./providers";
} from "@/app/admin/settings/interfaces";
import { AppProvider } from "@/components-2/context/AppContext";
import { PHProvider } from "@/app/providers";
import { getAuthTypeMetadataSS, getCurrentUserSS } from "@/lib/userSS";
import { Suspense } from "react";
import PostHogPageView from "./PostHogPageView";
import PostHogPageView from "@/app/PostHogPageView";
import Script from "next/script";
import { Hanken_Grotesk } from "next/font/google";
import { WebVitals } from "./web-vitals";
import { WebVitals } from "@/app/web-vitals";
import { ThemeProvider } from "next-themes";
import { DocumentsProvider } from "./chat/my-documents/DocumentsContext";
import { DocumentsProvider } from "@/app/chat/my-documents/DocumentsContext";
import CloudError from "@/components/errorPages/CloudErrorPage";
import Error from "@/components/errorPages/ErrorPage";
import AccessRestrictedPage from "@/components/errorPages/AccessRestrictedPage";
import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
import { fetchAssistantData as fetchAgentsData } from "@/lib/chat/fetchAssistantdata";
import { TooltipProvider } from "@/components/ui/tooltip";
const inter = Inter({
@@ -67,18 +67,17 @@ export async function generateMetadata(): Promise<Metadata> {
export const dynamic = "force-dynamic";
export default async function RootLayout({
children,
}: {
interface LayoutProps {
children: React.ReactNode;
}) {
const [combinedSettings, assistants, user, authTypeMetadata] =
await Promise.all([
fetchSettingsSS(),
fetchAssistantData(),
getCurrentUserSS(),
getAuthTypeMetadataSS(),
]);
}
export default async function Layout({ children }: LayoutProps) {
const [combinedSettings, agents, user, authTypeMetadata] = await Promise.all([
fetchSettingsSS(),
fetchAgentsData(),
getCurrentUserSS(),
getAuthTypeMetadataSS(),
]);
const productGating =
combinedSettings?.settings.application_status ?? ApplicationStatus.ACTIVE;
@@ -128,7 +127,7 @@ export default async function RootLayout({
enableSystem
disableTransitionOnChange
>
<div className="text-text min-h-screen bg-background">
<div className="text-text min-h-screen bg-background-tint-01">
<TooltipProvider>
<PHProvider>{content}</PHProvider>
</TooltipProvider>
@@ -153,13 +152,15 @@ export default async function RootLayout({
authTypeMetadata={authTypeMetadata}
user={user}
settings={combinedSettings}
assistants={assistants}
agents={agents}
>
<DocumentsProvider>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
{children}
<div id={MODAL_ROOT_ID} className="h-screen w-screen">
{children}
</div>
{process.env.NEXT_PUBLIC_POSTHOG_KEY && <WebVitals />}
</DocumentsProvider>
</AppProvider>

View File

@@ -0,0 +1,143 @@
const fonts = {
// Heading
headingH1: "font-heading-h1",
headingH2: "font-heading-h2",
headingH3: "font-heading-h3",
headingH3Muted: "font-heading-h3-muted",
// Main
mainBody: "font-main-body",
mainMuted: "font-main-muted",
mainAction: "font-main-action",
mainMono: "font-main-mono",
// Secondary
secondaryBody: "font-secondary-body",
secondaryAction: "font-secondary-action",
secondaryMono: "font-secondary-mono",
// Figure
figureSmallLabel: "font-figure-small-label",
figureSmallValue: "font-figure-small-value",
figureSmallKeystroke: "font-figure-small-keystroke",
};
const colors = {
text05: "text-text-05",
text04: "text-text-04",
text03: "text-text-03",
text02: "text-text-02",
text01: "text-text-01",
inverted: {
text05: "text-text-inverted-05",
text04: "text-text-inverted-04",
text03: "text-text-inverted-03",
text02: "text-text-inverted-02",
text01: "text-text-inverted-01",
},
};
export interface TextProps extends React.HTMLAttributes<HTMLElement> {
nowrap?: boolean;
// Fonts
headingH1?: boolean;
headingH2?: boolean;
headingH3?: boolean;
headingH3Muted?: boolean;
mainBody?: boolean;
mainMuted?: boolean;
mainAction?: boolean;
mainMono?: boolean;
secondaryBody?: boolean;
secondaryAction?: boolean;
secondaryMono?: boolean;
figureSmallLabel?: boolean;
figureSmallValue?: boolean;
figureSmallKeystroke?: boolean;
// Colors
text05?: boolean;
text04?: boolean;
text03?: boolean;
text02?: boolean;
text01?: boolean;
inverted?: boolean;
}
export default function Text({
nowrap,
headingH1,
headingH2,
headingH3,
headingH3Muted,
mainBody,
mainMuted,
mainAction,
mainMono,
secondaryBody,
secondaryAction,
secondaryMono,
figureSmallLabel,
figureSmallValue,
figureSmallKeystroke,
text05,
text04,
text03,
text02,
text01,
inverted,
children,
className,
}: TextProps) {
const font = headingH1
? "headingH1"
: headingH2
? "headingH2"
: headingH3
? "headingH3"
: headingH3Muted
? "headingH3Muted"
: mainBody
? "mainBody"
: mainMuted
? "mainMuted"
: mainAction
? "mainAction"
: mainMono
? "mainMono"
: secondaryBody
? "secondaryBody"
: secondaryAction
? "secondaryAction"
: secondaryMono
? "secondaryMono"
: figureSmallLabel
? "figureSmallLabel"
: figureSmallValue
? "figureSmallValue"
: figureSmallKeystroke
? "figureSmallKeystroke"
: "mainBody";
const color = text01
? "text01"
: text02
? "text02"
: text03
? "text03"
: text04
? "text04"
: text05
? "text05"
: "text05";
return (
<p
className={`${fonts[font]} ${inverted ? colors.inverted[color] : colors[color]} ${nowrap && "whitespace-nowrap"} ${className}`}
>
{children}
</p>
);
}

View File

@@ -0,0 +1,95 @@
import React, { useState, useRef, useLayoutEffect } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import Text, { TextProps } from "./Text";
interface TruncatedProps extends TextProps {
tooltipSide?: "top" | "right" | "bottom" | "left";
tooltipSideOffset?: number;
disableTooltip?: boolean;
}
/**
* Renders passed in text on a single line. If text is truncated,
* shows a tooltip on hover with the full text.
*/
export default function Truncated({
tooltipSide = "top",
tooltipSideOffset = 5,
disableTooltip,
children,
...rest
}: TruncatedProps) {
const [isTruncated, setIsTruncated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const visibleRef = useRef<HTMLDivElement>(null);
const hiddenRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
function checkTruncation() {
if (visibleRef.current && hiddenRef.current) {
const visibleWidth = visibleRef.current.offsetWidth;
const fullTextWidth = hiddenRef.current.offsetWidth;
setIsTruncated(fullTextWidth > visibleWidth);
setIsLoading(false);
}
}
// Reset loading state when children change
setIsLoading(true);
// Use a small delay to ensure DOM is ready
const timeoutId = setTimeout(checkTruncation, 0);
window.addEventListener("resize", checkTruncation);
return () => {
clearTimeout(timeoutId);
window.removeEventListener("resize", checkTruncation);
};
}, []);
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
ref={visibleRef}
className="flex-grow overflow-hidden text-left w-full"
>
{isLoading ? (
<div
className={`h-[1.2rem] w-full bg-background-tint-03 rounded animate-pulse ${rest.className}`}
/>
) : (
<Text
className={`line-clamp-1 break-all text-left ${rest.className}`}
{...rest}
>
{children}
</Text>
)}
</div>
</TooltipTrigger>
{/* Hide offscreen to measure full text width */}
<div
ref={hiddenRef}
className="fixed left-[-9999px] top-[0rem] whitespace-nowrap pointer-events-none opacity-0"
aria-hidden="true"
>
{children}
</div>
{!disableTooltip && isTruncated && !isLoading && (
<TooltipContent side={tooltipSide} sideOffset={tooltipSideOffset}>
<Text>{children}</Text>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import React from "react";
import Text from "@/components-2/Text";
const variantClasses = {
primary: "bg-theme-primary-05 hover:bg-theme-primary-04",
secondary: "bg-background-tint-01 hover:bg-background-tint-02",
danger: "bg-action-danger-05 hover:bg-action-danger-04",
} as const;
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
primary?: boolean;
secondary?: boolean;
danger?: boolean;
}
export default function Button({
children,
className,
primary,
secondary,
danger,
...props
}: ButtonProps) {
const variant = primary
? "primary"
: secondary
? "secondary"
: danger
? "danger"
: "primary";
return (
<button
className={`p-spacing-interline rounded-08 border ${variantClasses[variant]} ${className}`}
{...props}
>
{typeof children === "string" ? (
<Text inverted={variant === "primary"}>{children}</Text>
) : (
children
)}
</button>
);
}

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import Link from "next/link";
import Text from "@/components-2/Text";
import { SvgProps } from "@/icons";
export interface MenuButtonProps {
children?: string;
onClick?: () => void;
href?: string;
icon?: React.FunctionComponent<SvgProps>;
danger?: boolean;
}
export function MenuButton({
children,
onClick,
href,
icon: Icon,
danger,
}: MenuButtonProps) {
const content = (
<button
className="flex p-padding-button gap-spacing-interline rounded-08 hover:bg-background-tint-02 w-full"
onClick={(event) => {
event.stopPropagation();
onClick?.();
}}
>
{Icon && (
<Icon
className={`h-[1.2rem] min-w-[1.2rem] stroke-text-04 ${danger && "!stroke-action-danger-05"}`}
/>
)}
<Text text04 className={danger ? "!text-action-danger-05" : undefined}>
{children}
</Text>
</button>
);
if (!href) return content;
return <Link href={href}>{content}</Link>;
}

View File

@@ -0,0 +1,155 @@
"use client";
import React, {
createContext,
useState,
useContext,
useMemo,
useEffect,
useRef,
Dispatch,
SetStateAction,
} from "react";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { useSearchParams } from "next/navigation";
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
async function fetchAllAgents(): Promise<MinimalPersonaSnapshot[]> {
try {
const response = await fetch("/api/persona", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) throw new Error("Failed to fetch agents");
const agents: MinimalPersonaSnapshot[] = await response.json();
return agents;
} catch (error) {
console.error("Error fetching agents:", error);
return [];
}
}
async function pinAgents(pinnedAgentIds: number[]) {
console.log(pinnedAgentIds);
const response = await fetch(`/api/user/pinned-assistants`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
ordered_assistant_ids: pinnedAgentIds,
}),
});
if (!response.ok) {
throw new Error("Failed to update pinned assistants");
}
}
function getPinnedAgents(
agents: MinimalPersonaSnapshot[],
pinnedAgentIds?: number[]
): MinimalPersonaSnapshot[] {
return pinnedAgentIds
? (pinnedAgentIds
.map((pinnedAgentId) =>
agents.find((agent) => agent.id === pinnedAgentId)
)
.filter((agent) => !!agent) as MinimalPersonaSnapshot[])
: agents.filter((agent) => agent.is_default_persona && agent.id !== 0);
}
interface AgentsProviderProps {
agents: MinimalPersonaSnapshot[];
pinnedAgentIds: number[];
children: React.ReactNode;
}
export function AgentsProvider({
agents: initialAgents,
pinnedAgentIds: initialPinnedAgentIds,
children,
}: AgentsProviderProps) {
const [agents, setAgents] = useState<MinimalPersonaSnapshot[]>(initialAgents);
const [pinnedAgents, setPinnedAgents] = useState<MinimalPersonaSnapshot[]>(
() => getPinnedAgents(agents, initialPinnedAgentIds)
);
const isInitialMount = useRef(true);
const searchParams = useSearchParams();
const currentAgentIdRaw = searchParams?.get(SEARCH_PARAM_NAMES.PERSONA_ID);
const currentAgentId = currentAgentIdRaw ? parseInt(currentAgentIdRaw) : null;
const currentAgent = useMemo(
() =>
currentAgentId
? agents.find((agent) => agent.id === currentAgentId) || null
: null,
[agents, currentAgentId]
);
const unifiedAgent = agents.find((agent) => agent.id === 0) || null;
async function refreshAgents() {
setAgents(await fetchAllAgents());
}
function togglePinnedAgent(
agent: MinimalPersonaSnapshot,
shouldPin: boolean
) {
setPinnedAgents((prev) =>
shouldPin
? [...prev, agent]
: prev.filter((prevAgent) => prevAgent.id !== agent.id)
);
}
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
pinAgents(pinnedAgents.map((agent) => agent.id));
}, [pinnedAgents]);
return (
<AgentsContext.Provider
value={{
agents,
refreshAgents,
pinnedAgents,
setPinnedAgents,
togglePinnedAgent,
currentAgent,
unifiedAgent,
}}
>
{children}
</AgentsContext.Provider>
);
}
interface AgentsContextProps {
// All available agents
agents: MinimalPersonaSnapshot[];
refreshAgents: () => Promise<void>;
// Pinned agents (from user preferences)
pinnedAgents: MinimalPersonaSnapshot[];
setPinnedAgents: Dispatch<SetStateAction<MinimalPersonaSnapshot[]>>;
togglePinnedAgent: (agent: MinimalPersonaSnapshot, request: boolean) => void;
// Specific agents
currentAgent: MinimalPersonaSnapshot | null;
unifiedAgent: MinimalPersonaSnapshot | null;
}
const AgentsContext = createContext<AgentsContextProps | undefined>(undefined);
export function useAgentsContext(): AgentsContextProps {
const context = useContext(AgentsContext);
if (!context)
throw new Error("useAgentsContext must be used within an AgentsProvider");
return context;
}

View File

@@ -0,0 +1,50 @@
import { CombinedSettings } from "@/app/admin/settings/interfaces";
import { UserProvider } from "@/components/user/UserProvider";
import { ProviderContextProvider } from "@/components/chat/ProviderContext";
import { SettingsProvider } from "@/components/settings/SettingsProvider";
import { AssistantsProvider } from "@/components/context/AssistantsContext";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { User } from "@/lib/types";
import { ModalProvider } from "@/components/context/ModalContext";
import { ModalProvider as NewModalProvider } from "@/components-2/context/ModalContext";
import { AuthTypeMetadata } from "@/lib/userSS";
import { AgentsProvider } from "@/components-2/context/AgentsContext";
interface AppProviderProps {
children: React.ReactNode;
user: User | null;
settings: CombinedSettings;
agents: MinimalPersonaSnapshot[];
authTypeMetadata: AuthTypeMetadata;
}
export function AppProvider({
children,
user,
settings,
agents,
authTypeMetadata,
}: AppProviderProps) {
return (
<SettingsProvider settings={settings}>
<UserProvider
settings={settings}
user={user}
authTypeMetadata={authTypeMetadata}
>
<ProviderContextProvider>
<AssistantsProvider initialAssistants={agents}>
<ModalProvider user={user}>
<AgentsProvider
agents={agents}
pinnedAgentIds={user?.preferences.pinned_assistants || []}
>
<NewModalProvider>{children}</NewModalProvider>
</AgentsProvider>
</ModalProvider>
</AssistantsProvider>
</ProviderContextProvider>
</UserProvider>
</SettingsProvider>
);
}

View File

@@ -0,0 +1,96 @@
"use client";
import React, {
createContext,
useContext,
useState,
ReactNode,
Dispatch,
SetStateAction,
useEffect,
} from "react";
import Cookies from "js-cookie";
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
function setFoldedState(folded: boolean) {
const foldedAsString = folded.toString();
Cookies.set(SIDEBAR_TOGGLED_COOKIE_NAME, foldedAsString, { expires: 365 });
if (typeof window !== "undefined") {
localStorage.setItem(SIDEBAR_TOGGLED_COOKIE_NAME, foldedAsString);
}
}
export interface AppSidebarProviderProps {
folded: boolean;
children: ReactNode;
}
export function AppSidebarProvider({
folded: initiallyFolded,
children,
}: AppSidebarProviderProps) {
const [folded, setFolded] = useState(() => {
setFoldedState(initiallyFolded);
return initiallyFolded;
});
const [hovered, setHovered] = useState(false);
useEffect(() => {
setFoldedState(folded);
}, [folded]);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
const isMac = navigator.userAgent.toLowerCase().includes("mac");
const isModifierPressed = isMac ? event.metaKey : event.ctrlKey;
if (!isModifierPressed || event.key !== "e") return;
event.preventDefault();
setFolded((prev) => {
const newState = !prev;
setFoldedState(newState);
return newState;
});
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<AppSidebarContext.Provider
value={{
folded,
setFolded,
foldedAndHovered: folded && hovered,
setHovered,
}}
>
{children}
</AppSidebarContext.Provider>
);
}
export interface AppSidebarContextType {
folded: boolean;
setFolded: Dispatch<SetStateAction<boolean>>;
foldedAndHovered: boolean;
setHovered: Dispatch<SetStateAction<boolean>>;
}
const AppSidebarContext = createContext<AppSidebarContextType | undefined>(
undefined
);
export function useAppSidebarContext() {
const context = useContext(AppSidebarContext);
if (context === undefined) {
throw new Error(
"useAppSidebarContext must be used within an AppSidebarProvider"
);
}
return context;
}

View File

@@ -0,0 +1,53 @@
"use client";
import { useEscape } from "@/hooks/useEscape";
import { createContext, useContext, useState, ReactNode } from "react";
export enum ModalIds {
AgentsModal = "AgentsModal",
UserSettingsModal = "UserSettingsModal",
}
interface ModalProviderProps {
children: ReactNode;
}
export function ModalProvider({ children }: ModalProviderProps) {
const [openModal, setOpenModal] = useState<string | undefined>();
function toggleModal(id: ModalIds, open: boolean) {
if (openModal !== undefined) {
if (openModal === id && !open) setOpenModal(undefined);
else if (openModal !== id && open) setOpenModal(id);
} else {
if (open) setOpenModal(id);
}
}
function isOpen(id: string): boolean {
return openModal === id;
}
useEscape(() => setOpenModal(undefined));
return (
<ModalContext.Provider value={{ isOpen, toggleModal }}>
{children}
</ModalContext.Provider>
);
}
interface ModalContextType {
isOpen: (id: ModalIds) => boolean;
toggleModal: (id: ModalIds, open: boolean) => void;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
export function useModal() {
const context = useContext(ModalContext);
if (context === undefined) {
throw new Error("useModal must be used within a ModalProvider");
}
return context;
}

View File

@@ -0,0 +1,59 @@
import React from "react";
import { SvgProps } from "@/icons";
import Text from "@/components-2/Text";
import SvgX from "@/icons/x";
import CoreModal from "@/components-2/modals/CoreModal";
import { useEscape } from "@/hooks/useEscape";
interface ConfirmationModalProps {
icon: React.FunctionComponent<SvgProps>;
title: string;
escapeToClose?: boolean;
clickOutsideToClose?: boolean;
onClose: () => void;
description?: React.ReactNode;
children?: React.ReactNode;
}
export default function ConfirmationModal({
icon: Icon,
title,
escapeToClose = true,
clickOutsideToClose = true,
onClose,
description,
children,
}: ConfirmationModalProps) {
useEscape(onClose, escapeToClose);
return (
<CoreModal
className="z-10 w-[27rem] rounded-16 border flex flex-col bg-background-tint-00"
onClickOutside={clickOutsideToClose ? () => onClose?.() : undefined}
>
<div className="flex flex-col items-center justify-center p-spacing-paragraph gap-spacing-inline">
<div className="h-[1.5rem] flex flex-row justify-between items-center w-full">
<Icon className="w-[1.2rem] h-[1.2rem] stroke-text-04" />
<SvgX
className="stroke-text-03 w-[1.2rem] h-[1.2rem] hover:stroke-text-02"
onClick={onClose}
/>
</div>
<Text headingH3 text04 className="w-full text-left">
{title}
</Text>
</div>
{description && (
<div className="p-spacing-paragraph">
{typeof description === "string" ? (
<Text text03>{description}</Text>
) : (
description
)}
</div>
)}
{children && <div className="p-spacing-paragraph">{children}</div>}
</CoreModal>
);
}

View File

@@ -0,0 +1,45 @@
import React from "react";
import ReactDOM from "react-dom";
import { MODAL_ROOT_ID } from "@/lib/constants";
interface CoreModalProps {
onClickOutside?: () => void;
className?: string;
children?: React.ReactNode;
}
export default function CoreModal({
onClickOutside,
className,
children,
}: CoreModalProps) {
const insideModal = React.useRef(false);
// This must always exist.
const modalRoot = document.getElementById(MODAL_ROOT_ID);
if (!modalRoot)
throw new Error(
`A root div wrapping all children with the id ${MODAL_ROOT_ID} must exist, but was not found. This is an error. Go to "web/src/app/layout.tsx" and add a wrapper div with that id around the {children} invocation`
);
const modalContent = (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-mask-03 backdrop-blur-md"
onClick={() => (insideModal.current ? undefined : onClickOutside?.())}
>
<div
className={`z-10 rounded-16 border flex flex-col bg-background-tint-01 ${className}`}
onMouseOver={() => (insideModal.current = true)}
onMouseEnter={() => (insideModal.current = true)}
onMouseLeave={() => (insideModal.current = false)}
>
{children}
</div>
</div>
);
return ReactDOM.createPortal(
modalContent,
document.getElementById(MODAL_ROOT_ID)!
);
}

View File

@@ -0,0 +1,64 @@
import React, { useRef } from "react";
import Text from "@/components-2/Text";
import SvgX from "@/icons/x";
import { ModalIds, useModal } from "@/components-2/context/ModalContext";
interface ModalProps {
id: ModalIds;
title: string;
clickOutsideToClose?: boolean;
mini?: boolean;
className?: string;
children?: React.ReactNode;
}
export default function Modal({
id,
title,
clickOutsideToClose = true,
mini,
children,
className,
}: ModalProps) {
const { isOpen, toggleModal } = useModal();
const outsideModal = useRef(false);
if (!isOpen(id)) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-mask-03 backdrop-blur-md"
onClick={
clickOutsideToClose
? () => {
if (outsideModal.current) {
toggleModal(id, false);
}
}
: undefined
}
>
<div
className={`z-10 w-[80dvw] h-[80dvh] rounded-16 border flex flex-col bg-background-tint-01 ${mini && "max-w-[60rem]"} ${className}`}
onMouseOver={() => (outsideModal.current = false)}
onMouseLeave={() => (outsideModal.current = true)}
>
{/* Header with title */}
<div className="flex items-center justify-between p-padding-block-end">
<Text headingH2>{title}</Text>
<SvgX
className="stroke-text-03 w-[1.5rem] h-[1.5rem]"
onClick={() => toggleModal(id, false)}
/>
</div>
<div className="border-b" />
{/* Content area */}
<div className="flex-1 m-padding-block-end overflow-scroll">
{children}
</div>
</div>
</div>
);
}

View File

@@ -1,16 +1,18 @@
interface BasicClickableProps {
children: string | JSX.Element;
onClick?: () => void;
inset?: boolean;
fullWidth?: boolean;
className?: string;
}
export function BasicClickable({
children,
onClick,
fullWidth = false,
inset,
className,
}: {
children: string | JSX.Element;
onClick?: () => void;
inset?: boolean;
fullWidth?: boolean;
className?: string;
}) {
}: BasicClickableProps) {
return (
<button
onClick={onClick}
@@ -19,7 +21,7 @@ export function BasicClickable({
border-border
rounded
font-medium
text-text-darker
text-text-02
text-sm
relative
px-1 py-1.5
@@ -27,7 +29,7 @@ export function BasicClickable({
bg-background
select-none
overflow-hidden
hover:bg-accent-background
hover:bg-background-tint-01
${fullWidth ? "w-full" : ""}
${className ? className : ""}
`}
@@ -37,17 +39,19 @@ export function BasicClickable({
);
}
interface EmphasizedClickableProps {
children: string | JSX.Element;
onClick?: () => void;
fullWidth?: boolean;
size?: "sm" | "md" | "lg";
}
export function EmphasizedClickable({
children,
onClick,
fullWidth = false,
size = "md",
}: {
children: string | JSX.Element;
onClick?: () => void;
fullWidth?: boolean;
size?: "sm" | "md" | "lg";
}) {
}: EmphasizedClickableProps) {
return (
<button
className={`
@@ -64,11 +68,11 @@ export function EmphasizedClickable({
: `min-h-[42px] py-2 px-4`
}
w-fit
bg-accent-background-hovered
border-1 border-border-medium border bg-background-100
bg-background-tint-02
border-1 border-border-02 border bg-background-neutral-01
text-sm
rounded-lg
hover:bg-background-125
hover:bg-background-tint-03
`}
onClick={onClick}
>
@@ -77,6 +81,17 @@ export function EmphasizedClickable({
);
}
interface BasicSelectableProps {
children: string | JSX.Element;
selected: boolean;
hasBorder?: boolean;
fullWidth?: boolean;
removeColors?: boolean;
padding?: "none" | "normal" | "extra";
isDragging?: boolean;
isHovered?: boolean;
}
export function BasicSelectable({
children,
selected,
@@ -86,16 +101,7 @@ export function BasicSelectable({
removeColors = false,
isDragging = false,
isHovered,
}: {
children: string | JSX.Element;
selected: boolean;
hasBorder?: boolean;
fullWidth?: boolean;
removeColors?: boolean;
padding?: "none" | "normal" | "extra";
isDragging?: boolean;
isHovered?: boolean;
}) {
}: BasicSelectableProps) {
return (
<div
className={`
@@ -111,12 +117,12 @@ export function BasicSelectable({
${
!removeColors
? isDragging
? "bg-background-chat-hover"
? "bg-background-tint-02"
: selected
? "bg-background-chat-selected"
? "bg-background-tint-01"
: isHovered
? "bg-background-chat-hover"
: "hover:bg-background-chat-hover"
? "bg-background-tint-01"
: ""
: ""
}
${fullWidth ? "w-full" : ""}`}

View File

@@ -2,43 +2,53 @@ import { IconType } from "react-icons";
const ICON_SIZE = 15;
export const Hoverable: React.FC<{
export interface HoverableProps {
icon: IconType;
onClick?: () => void;
size?: number;
active?: boolean;
hoverText?: string;
}> = ({ icon: Icon, active, hoverText, onClick, size = ICON_SIZE }) => {
}
export function Hoverable({
icon: Icon,
active,
hoverText,
onClick,
size = ICON_SIZE,
}: HoverableProps) {
return (
<div
className={`group relative flex items-center overflow-hidden p-1.5 h-fit rounded-md cursor-pointer transition-all duration-300 ease-in-out hover:bg-accent-background-hovered`}
className={`group relative flex items-center overflow-hidden p-1.5 h-fit rounded-md cursor-pointer transition-all duration-300 ease-in-out hover:bg-background-tint-01`}
onClick={onClick}
>
<div className="flex items-center">
<Icon
size={size}
className="dark:text-[#B4B4B4] text-neutral-600 rounded h-fit cursor-pointer"
className="text-text-03 rounded h-fit cursor-pointer"
/>
{hoverText && (
<div className="max-w-0 leading-none whitespace-nowrap overflow-hidden transition-all duration-300 ease-in-out group-hover:max-w-xs group-hover:ml-2">
<span className="text-xs text-text-700">{hoverText}</span>
<span className="text-xs text-text-02">{hoverText}</span>
</div>
)}
</div>
</div>
);
};
}
export const HoverableIcon: React.FC<{
export interface HoverableIconProps {
icon: JSX.Element;
onClick?: () => void;
}> = ({ icon, onClick }) => {
}
export function HoverableIcon({ icon, onClick }: HoverableIconProps) {
return (
<div
className="hover:bg-background-chat-hover dark:text-[#B4B4B4] text-neutral-600 p-1.5 rounded h-fit cursor-pointer"
<button
className="hover:bg-background-tint-03 text-text-03 p-1.5 rounded h-fit"
onClick={onClick}
>
{icon}
</div>
</button>
);
};
}

View File

@@ -1,4 +1,5 @@
"use client";
import { Separator } from "@/components/ui/separator";
import { IconProps, XIcon } from "./icons/icons";
import { useRef } from "react";
@@ -8,8 +9,8 @@ import { cn } from "@/lib/utils";
interface ModalProps {
icon?: ({ size, className }: IconProps) => JSX.Element;
children: JSX.Element | string;
title?: JSX.Element | string;
children?: React.ReactNode;
title?: React.ReactNode;
onOutsideClick?: () => void;
className?: string;
width?: string;
@@ -52,19 +53,18 @@ export function Modal({
};
}, []);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
function handleMouseDown(e: React.MouseEvent<HTMLDivElement>) {
// Only close if the user clicked exactly on the overlay (and not on a child element).
if (onOutsideClick && e.target === e.currentTarget) {
onOutsideClick();
}
};
}
const modalContent = (
<div
onMouseDown={handleMouseDown}
className={cn(
`fixed inset-0 bg-neutral-950/50 border border-neutral-200 dark:border-neutral-800 bg-opacity-30 backdrop-blur-sm h-full
flex items-center justify-center z-50 transition-opacity duration-300 ease-in-out`
`fixed inset-0 bg-mask-01 border bg-opacity-30 backdrop-blur-md h-full flex items-center justify-center z-50 transition-opacity duration-300 ease-in-out`
)}
>
<div
@@ -75,9 +75,8 @@ export function Modal({
}
}}
className={`
bg-neutral-50 dark:bg-neutral-800
text-neutral-950 dark:text-neutral-50
rounded
bg-background-tint-02
rounded-08
shadow-2xl
transform
transition-all
@@ -89,7 +88,6 @@ export function Modal({
${className || ""}
flex
flex-col
${heightOverride ? `h-${heightOverride}` : "max-h-[90vh]"}
${hideOverflow ? "overflow-hidden" : "overflow-visible"}
`}
@@ -98,7 +96,7 @@ export function Modal({
<div className="absolute top-2 right-2">
<button
onClick={onOutsideClick}
className="cursor-pointer text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-300 transition-colors duration-200 p-2"
className="cursor-pointer transition-colors duration-200 p-2"
aria-label="Close modal"
>
<XIcon className="w-5 h-5" />

View File

@@ -3,6 +3,7 @@ import { AlertIcon } from "@/components/icons/icons";
import Link from "next/link";
import { SourceMetadata } from "@/lib/search/interfaces";
import React from "react";
import Text from "@/components-2/Text";
interface SourceTileProps {
sourceMetadata: SourceMetadata;
@@ -23,28 +24,28 @@ export default function SourceTile({
flex-col
items-center
justify-center
p-4
p-spacing-paragraph
rounded-lg
w-40
cursor-pointer
shadow-md
hover:bg-accent-background-hovered
relative
${
preSelect
? "bg-accent-background-hovered subtle-pulse"
: "bg-accent-background"
}
${preSelect ? "bg-background-tint-03 subtle-pulse" : "bg-background-tint-02"}
hover:bg-background-tint-03
gap-padding-button
`}
href={navigationUrl}
>
{sourceMetadata.federated && !hasExistingSlackCredentials && (
<div className="absolute -top-2 -left-2 z-10 bg-white rounded-full p-1 shadow-md border border-orange-200">
<AlertIcon size={18} className="text-orange-500 font-bold stroke-2" />
<div className="absolute -top-2 -left-2 z-10 bg-background-neutral-inverted-00 rounded-full p-1 shadow-md border border-status-warning-02">
<AlertIcon
size={18}
className="text-status-warning-05 font-bold stroke-2"
/>
</div>
)}
<SourceIcon sourceType={sourceMetadata.internalName} iconSize={24} />
<p className="font-medium text-sm mt-2">{sourceMetadata.displayName}</p>
<Text>{sourceMetadata.displayName}</Text>
</Link>
);
}

View File

@@ -26,20 +26,20 @@ export function TokenDisplay({
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-3 bg-neutral-100 dark:bg-neutral-800 rounded-full px-4 py-1.5">
<div className="hidden sm:block relative w-24 h-2 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<div className="flex items-center space-x-3 bg-background-tint-01 rounded-full px-4 py-1.5">
<div className="hidden sm:block relative w-24 h-2 bg-background-tint-03 rounded-full overflow-hidden">
<div
className={` absolute top-0 left-0 h-full rounded-full ${
tokenPercentage >= 100
? "bg-yellow-500 dark:bg-yellow-600"
: "bg-green-500 dark:bg-green-600"
? "bg-status-warning-05"
: "bg-status-success-05"
}`}
style={{
width: `${Math.min(tokenPercentage, 100)}%`,
}}
></div>
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-300 font-medium whitespace-nowrap">
<div className="text-xs text-text-03 whitespace-nowrap">
{totalTokens.toLocaleString()} / {maxTokens.toLocaleString()}{" "}
LLM tokens
</div>

View File

@@ -6,17 +6,22 @@ import Link from "next/link";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { UserRole } from "@/lib/types";
import { checkUserIsNoAuthUser, logout } from "@/lib/user";
import { Popover } from "./popover/Popover";
import { Popover } from "@/components/popover/Popover";
import { LOGOUT_DISABLED } from "@/lib/constants";
import { SettingsContext } from "./settings/SettingsProvider";
import { BellIcon, LightSettingsIcon, UserIcon } from "./icons/icons";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import {
BellIcon,
LightSettingsIcon,
UserIcon,
} from "@/components/icons/icons";
import { pageType } from "@/components/sidebar/types";
import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
import { useUser } from "./user/UserProvider";
import { Notifications } from "./chat/Notifications";
import DynamicFaIcon, { preloadIcons } from "@/components/icons/DynamicFaIcon";
import { useUser } from "@/components/user/UserProvider";
import { Notifications } from "@/components/chat/Notifications";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import Text from "@/components-2/Text";
interface DropdownOptionProps {
href?: string;
@@ -26,17 +31,17 @@ interface DropdownOptionProps {
openInNewTab?: boolean;
}
const DropdownOption: React.FC<DropdownOptionProps> = ({
function DropdownOption({
href,
onClick,
icon,
label,
openInNewTab,
}) => {
}: DropdownOptionProps) {
const content = (
<div className="flex py-1.5 text-sm px-2 gap-x-2 text-black text-sm cursor-pointer rounded hover:bg-background-300">
<div className="flex py-1.5 text-sm px-2 gap-x-2 text-sm cursor-pointer rounded hover:bg-background-tint-03">
{icon}
{label}
<Text>{label}</Text>
</div>
);
@@ -53,17 +58,19 @@ const DropdownOption: React.FC<DropdownOptionProps> = ({
} else {
return <div onClick={onClick}>{content}</div>;
}
};
}
interface UserDropdownProps {
page?: pageType;
toggleUserSettings?: () => void;
hideUserDropdown?: boolean;
}
export function UserDropdown({
page,
toggleUserSettings,
hideUserDropdown,
}: {
page?: pageType;
toggleUserSettings?: () => void;
hideUserDropdown?: boolean;
}) {
}: UserDropdownProps) {
const { user, isCurator } = useUser();
const [userInfoVisible, setUserInfoVisible] = useState(false);
const userInfoRef = useRef<HTMLDivElement>(null);
@@ -94,7 +101,7 @@ export function UserDropdown({
return null;
}
const handleLogout = () => {
function handleLogout() {
logout().then((isSuccess) => {
if (!isSuccess) {
alert("Failed to logout");
@@ -112,7 +119,7 @@ export function UserDropdown({
// Redirect to login page with the current page as a redirect parameter
router.push(`/auth/login?next=${encodedRedirect}`);
});
};
}
const showAdminPanel = !user || user.role === UserRole.ADMIN;
@@ -120,10 +127,10 @@ export function UserDropdown({
const showLogout =
user && !checkUserIsNoAuthUser(user.id) && !LOGOUT_DISABLED;
const onOpenChange = (open: boolean) => {
function onOpenChange(open: boolean) {
setUserInfoVisible(open);
setShowNotifications(false);
};
}
return (
<div className="group relative" ref={userInfoRef}>
@@ -136,13 +143,13 @@ export function UserDropdown({
onClick={() => setUserInfoVisible(!userInfoVisible)}
className="flex relative cursor-pointer"
>
<div
<button
className="
my-auto
bg-background-900
bg-background-tint-inverted-02
ring-2
ring-transparent
group-hover:ring-background-300/50
group-hover:ring-background-tint-03/50
transition-ring
duration-150
rounded-full
@@ -153,16 +160,18 @@ export function UserDropdown({
flex
items-center
justify-center
text-white
text-text-inverted-01
text-base
"
>
{user && user.email
? user.email[0] !== undefined && user.email[0].toUpperCase()
: "A"}
</div>
<Text inverted>
{user && user.email
? user.email[0] !== undefined && user.email[0].toUpperCase()
: "A"}
</Text>
</button>
{notifications && notifications.length > 0 && (
<div className="absolute -right-0.5 -top-0.5 w-3 h-3 bg-red-500 rounded-full"></div>
<div className="absolute -right-0.5 -top-0.5 w-3 h-3 bg-status-error-05 rounded-full" />
)}
</div>
}
@@ -171,18 +180,16 @@ export function UserDropdown({
className={`
p-2
${page != "admin" && showNotifications ? "w-72" : "w-[175px]"}
text-strong
text-text-01
text-sm
border
border-border
bg-background
dark:bg-[#2F2F2F]
border
bg-background-tint-01
rounded-lg
shadow-lg
flex
flex-col
max-h-96
overflow-y-auto
shadow-lg
flex
flex-col
max-h-96
overflow-y-auto
p-1
overscroll-contain
`}
@@ -196,7 +203,7 @@ export function UserDropdown({
) : hideUserDropdown ? (
<DropdownOption
onClick={() => router.push("/auth/login")}
icon={<UserIcon className="h-5w-5 my-auto " />}
icon={<UserIcon className="h-5 w-5 my-auto text-text-05" />}
label="Log In"
/>
) : (
@@ -230,7 +237,7 @@ export function UserDropdown({
) : (
<DynamicFaIcon
name={item.icon!}
className="h-4 w-4 my-auto "
className="h-4 w-4 my-auto text-text-05"
/>
)
}
@@ -242,14 +249,24 @@ export function UserDropdown({
{showAdminPanel ? (
<DropdownOption
href="/admin/indexing/status"
icon={<LightSettingsIcon size={16} className="my-auto" />}
icon={
<LightSettingsIcon
size={16}
className="my-auto text-text-05"
/>
}
label="Admin Panel"
/>
) : (
showCuratorPanel && (
<DropdownOption
href="/admin/indexing/status"
icon={<LightSettingsIcon size={16} className="my-auto" />}
icon={
<LightSettingsIcon
size={16}
className="my-auto text-text-05"
/>
}
label="Curator Panel"
/>
)
@@ -258,7 +275,9 @@ export function UserDropdown({
{toggleUserSettings && (
<DropdownOption
onClick={toggleUserSettings}
icon={<UserIcon size={16} className="my-auto" />}
icon={
<UserIcon size={16} className="my-auto text-text-05" />
}
label="User Settings"
/>
)}
@@ -268,7 +287,7 @@ export function UserDropdown({
setUserInfoVisible(true);
setShowNotifications(true);
}}
icon={<BellIcon size={16} className="my-auto" />}
icon={<BellIcon size={16} className="my-auto text-text-05" />}
label={`Notifications ${
notifications && notifications.length > 0
? `(${notifications.length})`
@@ -280,13 +299,15 @@ export function UserDropdown({
(showCuratorPanel ||
showAdminPanel ||
customNavItems.length > 0) && (
<div className="border-t border-border my-1" />
<div className="border-t my-1" />
)}
{showLogout && (
<DropdownOption
onClick={handleLogout}
icon={<FiLogOut size={16} className="my-auto" />}
icon={
<FiLogOut size={16} className="my-auto text-text-05" />
}
label="Log out"
/>
)}

View File

@@ -1,19 +1,13 @@
import { cn } from "@/lib/utils";
// Used for all admin page sections
export default function CardSection({
children,
className,
}: {
interface CardSectionProps {
children: React.ReactNode;
className?: string;
}) {
}
// Used for all admin page sections
export default function CardSection({ children, className }: CardSectionProps) {
return (
<div
className={cn(
"p-6 border bg-[#fff] dark:bg-neutral-800 rounded border-neutral-200 dark:border-neutral-700",
className
)}
className={`p-padding-content border bg-background-tint-02 rounded-08 ${className}`}
>
{children}
</div>

View File

@@ -1,6 +1,6 @@
"use client";
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
import { AdminSidebar } from "@/sections/sidebar/AdminSidebar";
import {
ClipboardIcon,
NotebookIconSkeleton,
@@ -26,15 +26,10 @@ import {
} from "@/components/icons/icons";
import { UserRole } from "@/lib/types";
import { FiActivity, FiBarChart2 } from "react-icons/fi";
import { UserDropdown } from "../UserDropdown";
import { User } from "@/lib/types";
import { usePathname } from "next/navigation";
import { SettingsContext } from "../settings/SettingsProvider";
import { useContext, useState } from "react";
import { useSettingsContext } from "@/components/settings/SettingsProvider";
import { MdOutlineCreditCard } from "react-icons/md";
import { UserSettingsModal } from "@/app/chat/components/modal/UserSettingsModal";
import { usePopup } from "./connectors/Popup";
import { useChatContext } from "../context/ChatContext";
import {
ApplicationStatus,
CombinedSettings,
@@ -42,56 +37,35 @@ import {
import Link from "next/link";
import { Button } from "../ui/button";
import { useIsKGExposed } from "@/app/admin/kg/utils";
import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus";
import { useCustomAnalyticsEnabled } from "@/lib/hooks/useCustomAnalyticsEnabled";
const connectors_items = () => [
{
name: (
<div className="flex">
<NotebookIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Existing Connectors</div>
</div>
),
name: "Existing Connectors",
icon: NotebookIconSkeleton,
link: "/admin/indexing/status",
},
{
name: (
<div className="flex">
<ConnectorIconSkeleton className="text-text-700" size={18} />
<div className="ml-1.5">Add Connector</div>
</div>
),
name: "Add Connector",
icon: ConnectorIconSkeleton,
link: "/admin/add-connector",
},
];
const document_management_items = () => [
{
name: (
<div className="flex">
<DocumentSetIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Document Sets</div>
</div>
),
name: "Document Sets",
icon: DocumentSetIconSkeleton,
link: "/admin/documents/sets",
},
{
name: (
<div className="flex">
<ZoomInIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Explorer</div>
</div>
),
name: "Explorer",
icon: ZoomInIconSkeleton,
link: "/admin/documents/explorer",
},
{
name: (
<div className="flex">
<ThumbsUpIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Feedback</div>
</div>
),
name: "Feedback",
icon: ThumbsUpIconSkeleton,
link: "/admin/documents/feedback",
},
];
@@ -102,12 +76,8 @@ const custom_assistants_items = (
) => {
const items = [
{
name: (
<div className="flex">
<AssistantsIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Assistants</div>
</div>
),
name: "Assistants",
icon: AssistantsIconSkeleton,
link: "/admin/assistants",
},
];
@@ -115,21 +85,13 @@ const custom_assistants_items = (
if (!isCurator) {
items.push(
{
name: (
<div className="flex">
<SlackIconSkeleton className="text-text-700" />
<div className="ml-1">Slack Bots</div>
</div>
),
name: "Slack Bots",
icon: SlackIconSkeleton,
link: "/admin/bots",
},
{
name: (
<div className="flex">
<ToolIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Actions</div>
</div>
),
name: "Actions",
icon: ToolIconSkeleton,
link: "/admin/actions",
}
);
@@ -137,12 +99,8 @@ const custom_assistants_items = (
if (enableEnterprise) {
items.push({
name: (
<div className="flex">
<ClipboardIcon className="text-text-700" size={18} />
<div className="ml-1">Standard Answers</div>
</div>
),
name: "Standard Answers",
icon: ClipboardIcon,
link: "/admin/standard-answer",
});
}
@@ -176,12 +134,8 @@ const collections = (
name: "User Management",
items: [
{
name: (
<div className="flex">
<GroupsIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Groups</div>
</div>
),
name: "Groups",
icon: GroupsIconSkeleton,
link: "/admin/groups",
},
],
@@ -194,51 +148,31 @@ const collections = (
name: "Configuration",
items: [
{
name: (
<div className="flex">
<OnyxSparkleIcon className="text-text-700" size={18} />
<div className="ml-1">Default Assistant</div>
</div>
),
name: "Default Assistant",
icon: OnyxSparkleIcon,
link: "/admin/configuration/default-assistant",
},
{
name: (
<div className="flex">
<CpuIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">LLM</div>
</div>
),
name: "LLM",
icon: CpuIconSkeleton,
link: "/admin/configuration/llm",
},
{
error: settings?.settings.needs_reindexing,
name: (
<div className="flex">
<SearchIcon className="text-text-700" />
<div className="ml-1">Search Settings</div>
</div>
),
name: "Search Settings",
icon: SearchIcon,
link: "/admin/configuration/search",
},
{
name: (
<div className="flex">
<DocumentIcon2 className="text-text-700" />
<div className="ml-1">Document Processing</div>
</div>
),
name: "Document Processing",
icon: DocumentIcon2,
link: "/admin/configuration/document-processing",
},
...(kgExposed
? [
{
name: (
<div className="flex">
<BrainIcon className="text-text-700" />
<div className="ml-1">Knowledge Graph</div>
</div>
),
name: "Knowledge Graph",
icon: BrainIcon,
link: "/admin/kg",
},
]
@@ -249,46 +183,27 @@ const collections = (
name: "User Management",
items: [
{
name: (
<div className="flex">
<UsersIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Users</div>
</div>
),
name: "Users",
icon: UsersIconSkeleton,
link: "/admin/users",
},
...(enableEnterprise
? [
{
name: (
<div className="flex">
<GroupsIconSkeleton
className="text-text-700"
size={18}
/>
<div className="ml-1">Groups</div>
</div>
),
name: "Groups",
icon: GroupsIconSkeleton,
link: "/admin/groups",
},
]
: []),
{
name: (
<div className="flex">
<KeyIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">API Keys</div>
</div>
),
name: "API Keys",
icon: KeyIconSkeleton,
link: "/admin/api-key",
},
{
name: (
<div className="flex">
<ShieldIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Token Rate Limits</div>
</div>
),
name: "Token Rate Limits",
icon: ShieldIconSkeleton,
link: "/admin/token-rate-limits",
},
],
@@ -299,26 +214,15 @@ const collections = (
name: "Performance",
items: [
{
name: (
<div className="flex">
<FiActivity className="text-text-700" size={18} />
<div className="ml-1">Usage Statistics</div>
</div>
),
name: "Usage Statistics",
icon: FiActivity,
link: "/admin/performance/usage",
},
...(settings?.settings.query_history_type !== "disabled"
? [
{
name: (
<div className="flex">
<DatabaseIconSkeleton
className="text-text-700"
size={18}
/>
<div className="ml-1">Query History</div>
</div>
),
name: "Query History",
icon: DatabaseIconSkeleton,
link: "/admin/performance/query-history",
},
]
@@ -326,15 +230,8 @@ const collections = (
...(!enableCloud && customAnalyticsEnabled
? [
{
name: (
<div className="flex">
<FiBarChart2
className="text-text-700"
size={18}
/>
<div className="ml-1">Custom Analytics</div>
</div>
),
name: "Custom Analytics",
icon: FiBarChart2,
link: "/admin/performance/custom-analytics",
},
]
@@ -347,26 +244,15 @@ const collections = (
name: "Settings",
items: [
{
name: (
<div className="flex">
<SettingsIconSkeleton className="text-text-700" size={18} />
<div className="ml-1">Workspace Settings</div>
</div>
),
name: "Workspace Settings",
icon: SettingsIconSkeleton,
link: "/admin/settings",
},
...(enableEnterprise
? [
{
name: (
<div className="flex">
<PaintingIconSkeleton
className="text-text-700"
size={18}
/>
<div className="ml-1">Whitelabeling</div>
</div>
),
name: "Whitelabeling",
icon: PaintingIconSkeleton,
link: "/admin/whitelabeling",
},
]
@@ -374,15 +260,8 @@ const collections = (
...(enableCloud
? [
{
name: (
<div className="flex">
<MdOutlineCreditCard
className="text-text-700"
size={18}
/>
<div className="ml-1">Billing</div>
</div>
),
name: "Billing",
icon: MdOutlineCreditCard,
link: "/admin/billing",
},
]
@@ -411,19 +290,7 @@ export function ClientLayout({
user?.role === UserRole.CURATOR || user?.role === UserRole.GLOBAL_CURATOR;
const pathname = usePathname();
const settings = useContext(SettingsContext);
const [userSettingsOpen, setUserSettingsOpen] = useState(false);
const toggleUserSettings = () => {
setUserSettingsOpen(!userSettingsOpen);
};
const { llmProviders, ccPairs } = useChatContext();
const { popup, setPopup } = usePopup();
// Fetch federated-connector info so the modal can list/refresh them
const {
connectors: federatedConnectors,
refetch: refetchFederatedConnectors,
} = useFederatedOAuthStatus();
const settings = useSettingsContext();
if (isLoading) {
return <></>;
@@ -438,20 +305,6 @@ export function ClientLayout({
return (
<div className="h-screen w-screen flex overflow-y-hidden">
{popup}
{userSettingsOpen && (
<UserSettingsModal
llmProviders={llmProviders}
setPopup={setPopup}
onClose={() => setUserSettingsOpen(false)}
defaultModel={user?.preferences?.default_model!}
ccPairs={ccPairs}
federatedConnectors={federatedConnectors}
refetchFederatedConnectors={refetchFederatedConnectors}
/>
)}
{settings?.settings.application_status ===
ApplicationStatus.PAYMENT_REMINDER && (
<div className="fixed top-2 left-1/2 transform -translate-x-1/2 bg-amber-400 dark:bg-amber-500 text-gray-900 dark:text-gray-100 p-4 rounded-lg shadow-lg z-50 max-w-md text-center">
@@ -470,25 +323,20 @@ export function ClientLayout({
</div>
)}
<div className="default-scrollbar flex-none text-text-settings-sidebar bg-background-sidebar dark:bg-[#000] w-[250px] overflow-x-hidden z-20 pt-2 pb-8 h-full border-r border-border dark:border-none miniscroll overflow-auto">
<AdminSidebar
collections={collections(
isCurator,
enableCloud,
enableEnterprise,
settings,
kgExposed,
customAnalyticsEnabled
)}
/>
</div>
<div className="overflow-y-scroll w-full">
<div className="fixed left-0 gap-x-4 px-4 top-4 h-8 mb-auto w-full items-start flex justify-end">
<UserDropdown toggleUserSettings={toggleUserSettings} />
</div>
<div className="flex pt-10 pb-4 px-4 md:px-12">{children}</div>
<AdminSidebar
collections={collections(
isCurator,
enableCloud,
enableEnterprise,
settings,
kgExposed,
customAnalyticsEnabled
)}
/>
<div className="overflow-y-scroll w-full flex pt-10 pb-4 px-4 md:px-12">
{children}
</div>
</div>
);
// Is there a clean way to add this to some piece of text where we need to enbale for copy-paste in a react app?
}

View File

@@ -5,16 +5,20 @@ import {
getCurrentUserSS,
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import { ClientLayout } from "./ClientLayout";
import { ClientLayout } from "@/components/admin/ClientLayout";
import {
NEXT_PUBLIC_CLOUD_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
} from "@/lib/constants";
import { AnnouncementBanner } from "../header/AnnouncementBanner";
import { AnnouncementBanner } from "@/components/header/AnnouncementBanner";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { ChatProvider } from "../context/ChatContext";
import { ChatProvider } from "@/components/context/ChatContext";
export async function Layout({ children }: { children: React.ReactNode }) {
interface LayoutProps {
children: React.ReactNode;
}
export async function Layout({ children }: LayoutProps) {
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()];
// catch cases where the backend is completely unreachable here

View File

@@ -1,87 +0,0 @@
// Sidebar.tsx
"use client";
import React, { useContext } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { CgArrowsExpandUpLeft } from "react-icons/cg";
import { LogoComponent } from "@/components/logo/FixedLogo";
interface Item {
name: string | JSX.Element;
link: string;
error?: boolean;
}
interface Collection {
name: string | JSX.Element;
items: Item[];
}
export function AdminSidebar({ collections }: { collections: Collection[] }) {
const combinedSettings = useContext(SettingsContext);
const pathname = usePathname() ?? "";
if (!combinedSettings) {
return null;
}
const enterpriseSettings = combinedSettings.enterpriseSettings;
return (
<div className="text-text-settings-sidebar pl-0">
<nav className="space-y-2">
<div className="w-full ml-4 mt-1 h-8 justify-start mb-4 flex">
<LogoComponent
show={true}
enterpriseSettings={enterpriseSettings!}
backgroundToggled={false}
isAdmin={true}
/>
</div>
<div className="flex w-full justify-center">
<Link href="/chat">
<button className="text-sm text-text-700 hover:bg-background-settings-hover dark:hover:bg-neutral-800 flex items-center block w-52 py-2.5 flex px-2 text-left hover:bg-opacity-80 cursor-pointer rounded">
<CgArrowsExpandUpLeft className="my-auto" size={18} />
<p className="ml-1 break-words line-clamp-2 ellipsis leading-none">
Exit Admin
</p>
</button>
</Link>
</div>
{collections.map((collection, collectionInd) => (
<div
className="flex flex-col items-center justify-center w-full"
key={collectionInd}
>
<h2 className="text-xs text-text-800 w-52 font-bold pb-2">
<div>{collection.name}</div>
</h2>
{collection.items.map((item) => (
<Link key={item.link} href={item.link}>
<button
className={`text-sm text-text-700 block flex gap-x-2 items-center w-52 py-2.5 px-2 text-left hover:bg-background-settings-hover dark:hover:bg-neutral-800 rounded
${
pathname.startsWith(item.link)
? "bg-background-settings-hover dark:bg-neutral-700"
: ""
}`}
>
{item.name}
</button>
</Link>
))}
</div>
))}
</nav>
{combinedSettings.webVersion && (
<div
className="flex flex-col mt-12 items-center justify-center w-full"
key={"onyxVersion"}
>
<h2 className="text-xs text-text/40 w-52 font-medium">
Onyx version: {combinedSettings.webVersion}
</h2>
</div>
)}
</div>
);
}

View File

@@ -8,7 +8,6 @@ import Link from "next/link";
import { pageType } from "@/components/sidebar/types";
import { useRouter } from "next/navigation";
import { ChatBanner } from "@/app/chat/components/ChatBanner";
import LogoWithText from "../header/LogoWithText";
import { NewChatIcon } from "../icons/icons";
import { SettingsContext } from "../settings/SettingsProvider";
@@ -54,13 +53,13 @@ const FunctionalHeader = React.memo(function FunctionalHeader({
}`}
>
<div className="items-end flex mt-2 text-text-700 relative flex w-full">
<LogoWithText
{/* <LogoWithText
assistantId={currentChatSession?.persona_id}
page={page}
toggleSidebar={toggleSidebar}
toggled={false}
handleNewChat={handleNewChat}
/>
/> */}
<div className="mt-1 items-center flex w-full h-8">
<div
style={{ transition: "width 0.30s ease-out" }}
@@ -113,12 +112,12 @@ const FunctionalHeader = React.memo(function FunctionalHeader({
)}
<div className="invisible">
<LogoWithText
{/* <LogoWithText
page={page}
toggled={sidebarToggled}
toggleSidebar={toggleSidebar}
handleNewChat={handleNewChat}
/>
/> */}
</div>
<div className="absolute right-2 mobile:top-1 desktop:top-1 h-8 flex">

View File

@@ -1,42 +0,0 @@
"use client";
import { CombinedSettings } from "@/app/admin/settings/interfaces";
import { UserProvider } from "../user/UserProvider";
import { ProviderContextProvider } from "../chat/ProviderContext";
import { SettingsProvider } from "../settings/SettingsProvider";
import { AssistantsProvider } from "./AssistantsContext";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { User } from "@/lib/types";
import { ModalProvider } from "./ModalContext";
import { AuthTypeMetadata } from "@/lib/userSS";
interface AppProviderProps {
children: React.ReactNode;
user: User | null;
settings: CombinedSettings;
assistants: MinimalPersonaSnapshot[];
authTypeMetadata: AuthTypeMetadata;
}
export const AppProvider = ({
children,
user,
settings,
assistants,
authTypeMetadata,
}: AppProviderProps) => {
return (
<SettingsProvider settings={settings}>
<UserProvider
settings={settings}
user={user}
authTypeMetadata={authTypeMetadata}
>
<ProviderContextProvider>
<AssistantsProvider initialAssistants={assistants}>
<ModalProvider user={user}>{children}</ModalProvider>
</AssistantsProvider>
</ProviderContextProvider>
</UserProvider>
</SettingsProvider>
);
};

View File

@@ -13,9 +13,105 @@ import { Folder } from "@/app/chat/components/folders/interfaces";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { ToolSnapshot } from "@/lib/tools/interfaces";
import { SEARCH_PARAMS } from "@/lib/extension/constants";
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
// We use Omit to exclude 'refreshChatSessions' from the value prop type
// because we're defining it within the component
interface ChatProviderProps {
value: Omit<
ChatContextProps,
| "refreshChatSessions"
| "refreshAvailableAssistants"
| "reorderFolders"
| "refreshFolders"
| "refreshInputPrompts"
>;
children: React.ReactNode;
}
export function ChatProvider({ value, children }: ChatProviderProps) {
const router = useRouter();
const searchParams = useSearchParams();
const currentChatId = searchParams.get(SEARCH_PARAM_NAMES.CHAT_ID);
const [inputPrompts, setInputPrompts] = useState(value?.inputPrompts || []);
const [chatSessions, setChatSessions] = useState(value?.chatSessions || []);
const [folders, setFolders] = useState(value?.folders || []);
const currentChat =
value.chatSessions.find(
(chatSession) => chatSession.id === currentChatId
) || null;
const reorderFolders = (displayPriorityMap: Record<number, number>) => {
setFolders(
folders.map((folder) => {
if (folder.folder_id) {
const display_priority = displayPriorityMap[folder.folder_id];
if (display_priority !== undefined) {
folder.display_priority = display_priority;
}
}
return folder;
})
);
};
const refreshChatSessions = async () => {
try {
const response = await fetch("/api/chat/get-user-chat-sessions");
if (!response.ok) throw new Error("Failed to fetch chat sessions");
const { sessions } = await response.json();
setChatSessions(sessions);
if (
currentChatId &&
!sessions.some((session: ChatSession) => session.id === currentChatId)
) {
router.replace("/chat");
}
} catch (error) {
console.error("Error refreshing chat sessions:", error);
}
};
const refreshFolders = async () => {
const response = await fetch("/api/folder");
if (!response.ok) throw new Error("Failed to fetch folders");
const { folders } = await response.json();
setFolders(folders);
};
const refreshInputPrompts = async () => {
const response = await fetch("/api/input_prompt");
if (!response.ok) throw new Error("Failed to fetch input prompts");
const inputPrompts = await response.json();
setInputPrompts(inputPrompts);
};
return (
<ChatContext.Provider
value={{
...value,
inputPrompts,
refreshInputPrompts,
chatSessions,
currentChatId,
currentChat,
folders,
reorderFolders,
refreshChatSessions,
refreshFolders,
}}
>
{children}
</ChatContext.Provider>
);
}
interface ChatContextProps {
chatSessions: ChatSession[];
currentChatId: string | null;
currentChat: ChatSession | null;
sidebarInitiallyVisible: boolean;
availableSources: ValidSources[];
ccPairs: CCPairBasicInfo[];
@@ -40,94 +136,9 @@ interface ChatContextProps {
const ChatContext = createContext<ChatContextProps | undefined>(undefined);
// We use Omit to exclude 'refreshChatSessions' from the value prop type
// because we're defining it within the component
export const ChatProvider: React.FC<{
value: Omit<
ChatContextProps,
| "refreshChatSessions"
| "refreshAvailableAssistants"
| "reorderFolders"
| "refreshFolders"
| "refreshInputPrompts"
>;
children: React.ReactNode;
}> = ({ value, children }) => {
const router = useRouter();
const searchParams = useSearchParams();
const [inputPrompts, setInputPrompts] = useState(value?.inputPrompts || []);
const [chatSessions, setChatSessions] = useState(value?.chatSessions || []);
const [folders, setFolders] = useState(value?.folders || []);
const reorderFolders = (displayPriorityMap: Record<number, number>) => {
setFolders(
folders.map((folder) => {
if (folder.folder_id) {
const display_priority = displayPriorityMap[folder.folder_id];
if (display_priority !== undefined) {
folder.display_priority = display_priority;
}
}
return folder;
})
);
};
const refreshChatSessions = async () => {
try {
const response = await fetch("/api/chat/get-user-chat-sessions");
if (!response.ok) throw new Error("Failed to fetch chat sessions");
const { sessions } = await response.json();
setChatSessions(sessions);
const currentSessionId = searchParams?.get("chatId");
if (
currentSessionId &&
!sessions.some(
(session: ChatSession) => session.id === currentSessionId
)
) {
router.replace("/chat");
}
} catch (error) {
console.error("Error refreshing chat sessions:", error);
}
};
const refreshFolders = async () => {
const response = await fetch("/api/folder");
if (!response.ok) throw new Error("Failed to fetch folders");
const { folders } = await response.json();
setFolders(folders);
};
const refreshInputPrompts = async () => {
const response = await fetch("/api/input_prompt");
if (!response.ok) throw new Error("Failed to fetch input prompts");
const inputPrompts = await response.json();
setInputPrompts(inputPrompts);
};
return (
<ChatContext.Provider
value={{
...value,
inputPrompts,
refreshInputPrompts,
chatSessions,
folders,
reorderFolders,
refreshChatSessions,
refreshFolders,
}}
>
{children}
</ChatContext.Provider>
);
};
export const useChatContext = (): ChatContextProps => {
export function useChatContext(): ChatContextProps {
const context = useContext(ChatContext);
if (!context) {
if (!context)
throw new Error("useChatContext must be used within a ChatProvider");
}
return context;
};
}

View File

@@ -9,23 +9,15 @@ import {
import { FiExternalLink } from "react-icons/fi";
import CardSection from "../admin/CardSection";
export function ModelPreview({
model,
display,
showDetails = false,
}: {
interface ModelPreviewProps {
model: EmbeddingModelDescriptor;
display?: boolean;
showDetails?: boolean;
}) {
}
export function ModelPreview({ model }: ModelPreviewProps) {
const currentModelCopy = getCurrentModelCopy(model.model_name);
return (
<CardSection
className={`shadow-md ${
display ? "bg-inverted rounded-lg p-4" : "bg-accent-background p-2"
} w-96 flex flex-col`}
>
<CardSection className={`w-96 flex flex-col`}>
<div className="font-bold text-lg flex">{model.model_name}</div>
<div className="text-sm mt-1 mx-1 mb-3">
@@ -34,126 +26,122 @@ export function ModelPreview({
"Custom model—no description is available."}
</div>
{showDetails && (
<div className="pt-4 border-t border-border space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-semibold text-text-700">Dimensions:</span>
<div className="text-text-600">
{model.model_dim.toLocaleString()}
</div>
<div className="pt-4 border-t border-border space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-semibold text-text-700">Dimensions:</span>
<div className="text-text-600">
{model.model_dim.toLocaleString()}
</div>
</div>
<div>
<span className="font-semibold text-text-700">Provider:</span>
<div className="text-text-600">
{model.provider_type || "Self-hosted"}
</div>
<div>
<span className="font-semibold text-text-700">Provider:</span>
<div className="text-text-600">
{model.provider_type || "Self-hosted"}
</div>
</div>
<div>
<span className="font-semibold text-text-700">Normalized:</span>
<div className="text-text-600">
{model.normalize ? "Yes" : "No"}
</div>
<div>
<span className="font-semibold text-text-700">Normalized:</span>
<div className="text-text-600">
{model.normalize ? "Yes" : "No"}
</div>
</div>
{"embedding_precision" in model &&
(model as any).embedding_precision && (
<div>
<span className="font-semibold text-text-700">
Precision:
</span>
<div className="text-text-600">
{(model as any).embedding_precision}
</div>
{"embedding_precision" in model &&
(model as any).embedding_precision && (
<div>
<span className="font-semibold text-text-700">Precision:</span>
<div className="text-text-600">
{(model as any).embedding_precision}
</div>
)}
</div>
)}
{"isDefault" in model &&
(model as HostedEmbeddingModel).isDefault && (
<div>
<span className="font-semibold text-text-700">Type:</span>
<div className="text-text-600">Default</div>
</div>
)}
{"isDefault" in model &&
(model as HostedEmbeddingModel).isDefault && (
<div>
<span className="font-semibold text-text-700">Type:</span>
<div className="text-text-600">Default</div>
</div>
)}
{"pricePerMillion" in model && (
{"pricePerMillion" in model && (
<div>
<span className="font-semibold text-text-700">
Price/Million:
</span>
<div className="text-text-600">
${(model as CloudEmbeddingModel).pricePerMillion}
</div>
</div>
)}
</div>
{(model.query_prefix || model.passage_prefix) && (
<div className="space-y-2">
{model.query_prefix && (
<div>
<span className="font-semibold text-text-700">
Price/Million:
Query Prefix:
</span>
<div className="text-text-600">
${(model as CloudEmbeddingModel).pricePerMillion}
<div className="text-text-600 font-mono text-xs bg-background p-2 rounded">
&quot;{model.query_prefix}&quot;
</div>
</div>
)}
{model.passage_prefix && (
<div>
<span className="font-semibold text-text-700">
Passage Prefix:
</span>
<div className="text-text-600 font-mono text-xs bg-background p-2 rounded">
&quot;{model.passage_prefix}&quot;
</div>
</div>
)}
</div>
)}
{(model.query_prefix || model.passage_prefix) && (
<div className="space-y-2">
{model.query_prefix && (
<div>
<span className="font-semibold text-text-700">
Query Prefix:
</span>
<div className="text-text-600 font-mono text-xs bg-background p-2 rounded">
&quot;{model.query_prefix}&quot;
</div>
</div>
)}
{model.passage_prefix && (
<div>
<span className="font-semibold text-text-700">
Passage Prefix:
</span>
<div className="text-text-600 font-mono text-xs bg-background p-2 rounded">
&quot;{model.passage_prefix}&quot;
</div>
</div>
)}
{model.api_url && (
<div>
<span className="font-semibold text-text-700">API URL:</span>
<div className="text-text-600 font-mono text-xs bg-background p-2 rounded break-all">
{model.api_url}
</div>
)}
</div>
)}
{model.api_url && (
<div>
<span className="font-semibold text-text-700">API URL:</span>
<div className="text-text-600 font-mono text-xs bg-background p-2 rounded break-all">
{model.api_url}
</div>
</div>
)}
{model.api_version && (
<div>
<span className="font-semibold text-text-700">API Version:</span>
<div className="text-text-600">{model.api_version}</div>
</div>
)}
{model.api_version && (
<div>
<span className="font-semibold text-text-700">API Version:</span>
<div className="text-text-600">{model.api_version}</div>
</div>
)}
{model.deployment_name && (
<div>
<span className="font-semibold text-text-700">Deployment:</span>
<div className="text-text-600">{model.deployment_name}</div>
</div>
)}
{model.deployment_name && (
<div>
<span className="font-semibold text-text-700">Deployment:</span>
<div className="text-text-600">{model.deployment_name}</div>
</div>
)}
{"link" in model && (model as HostedEmbeddingModel).link && (
<div className="pt-2">
<a
href={(model as HostedEmbeddingModel).link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-blue-500 hover:text-blue-700 transition-colors duration-200 text-sm"
>
<span>View Documentation</span>
<FiExternalLink className="ml-1" size={14} />
</a>
</div>
)}
</div>
)}
{"link" in model && (model as HostedEmbeddingModel).link && (
<div className="pt-2">
<a
href={(model as HostedEmbeddingModel).link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-blue-500 hover:text-blue-700 transition-colors duration-200 text-sm"
>
<span>View Documentation</span>
<FiExternalLink className="ml-1" size={14} />
</a>
</div>
)}
</div>
</CardSection>
);
}
@@ -221,6 +209,7 @@ export function ModelOption({
</div>
);
}
export function ModelSelector({
modelOptions,
setSelectedModel,

View File

@@ -1,11 +0,0 @@
export function HeaderWrapper({
children,
}: {
children: JSX.Element | string;
}) {
return (
<header className="border-b border-border bg-background-emphasis">
<div className="mx-8 h-16">{children}</div>
</header>
);
}

View File

@@ -1,147 +0,0 @@
"use effect";
import { useContext } from "react";
import { FiSidebar } from "react-icons/fi";
import { SettingsContext } from "../settings/SettingsProvider";
import { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { pageType } from "@/components/sidebar/types";
import { Logo } from "../logo/Logo";
import Link from "next/link";
import { LogoComponent } from "@/components/logo/FixedLogo";
export default function LogoWithText({
toggleSidebar,
hideOnMobile,
handleNewChat,
page,
toggled,
showArrow,
assistantId,
explicitlyUntoggle = () => null,
}: {
hideOnMobile?: boolean;
toggleSidebar?: () => void;
handleNewChat?: () => void;
page: pageType;
toggled?: boolean;
showArrow?: boolean;
assistantId?: number;
explicitlyUntoggle?: () => void;
}) {
const combinedSettings = useContext(SettingsContext);
const enterpriseSettings = combinedSettings?.enterpriseSettings;
return (
<div
className={`${
hideOnMobile && "mobile:hidden"
} z-[100] ml-2 mt-1 h-8 mb-auto shrink-0 flex gap-x-0 items-center text-xl`}
>
{toggleSidebar && page == "chat" ? (
<div
onClick={() => toggleSidebar()}
className="flex gap-x-2 items-center ml-0 cursor-pointer desktop:hidden "
>
{!toggled ? (
<Logo className="desktop:hidden" height={24} width={24} />
) : (
<LogoComponent
show={toggled}
enterpriseSettings={enterpriseSettings!}
backgroundToggled={toggled}
/>
)}
<FiSidebar
size={20}
className={`text-text-mobile-sidebar desktop:hidden ${
toggled && "mobile:hidden"
}`}
/>
</div>
) : (
<div className="mr-1 invisible mb-auto h-6 w-6">
<Logo height={24} width={24} />
</div>
)}
{!toggled && (
<div
className={`${
showArrow ? "desktop:hidden" : "invisible"
} break-words inline-block w-fit text-text-700 dark:text-neutral-300 text-xl`}
>
<LogoComponent
enterpriseSettings={enterpriseSettings!}
backgroundToggled={toggled}
/>
</div>
)}
{page == "chat" && !showArrow && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
className="my-auto mobile:hidden"
href={
`/${page}` +
(assistantId ? `?assistantId=${assistantId}` : "")
}
onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
return;
}
if (handleNewChat) {
handleNewChat();
}
}}
>
<NewChatIcon
className="ml-2 flex-none text-text-700 hover:text-text-600 "
size={24}
/>
</Link>
</TooltipTrigger>
<TooltipContent>New Chat</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<div className="flex ml-auto gap-x-4">
{showArrow && toggleSidebar && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
className="mr-2 my-auto"
onClick={() => {
toggleSidebar();
if (toggled) {
explicitlyUntoggle();
}
}}
>
{!toggled && !combinedSettings?.isMobile ? (
<RightToLineIcon className="mobile:hidden text-sidebar-toggle" />
) : (
<LeftToLineIcon className="mobile:hidden text-sidebar-toggle" />
)}
<FiSidebar
size={20}
className="hidden mobile:block text-text-mobile-sidebar"
/>
</button>
</TooltipTrigger>
<TooltipContent className="!border-none">
{toggled ? `Unpin sidebar` : "Pin sidebar"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
);
}

View File

@@ -3152,6 +3152,7 @@ export const PinnedIcon = ({
</svg>
);
};
export const OnyxLogoTypeIcon = ({
size = 16,
className = defaultTailwindCSS,

View File

@@ -1,20 +1,22 @@
"use client";
import { useContext } from "react";
import { SettingsContext } from "../settings/SettingsProvider";
import { OnyxIcon, OnyxLogoTypeIcon } from "../icons/icons";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { OnyxIcon, OnyxLogoTypeIcon } from "@/components/icons/icons";
interface LogoProps {
height?: number;
width?: number;
className?: string;
size?: "small" | "default" | "large";
}
export function Logo({
height,
width,
className,
size = "default",
}: {
height?: number;
width?: number;
className?: string;
size?: "small" | "default" | "large";
}) {
}: LogoProps) {
const settings = useContext(SettingsContext);
const sizeMap = {
@@ -34,10 +36,7 @@ export function Logo({
) {
return (
<div style={{ height, width }} className={className}>
<OnyxIcon
size={height}
className={`${className} dark:text-[#fff] text-[#000]`}
/>
<OnyxIcon size={height} className={className} />
</div>
);
}
@@ -57,15 +56,10 @@ export function Logo({
);
}
export function LogoType({
size = "default",
}: {
interface LogoTypeProps {
size?: "small" | "default" | "large";
}) {
return (
<OnyxLogoTypeIcon
size={115}
className={`items-center w-full dark:text-[#fff]`}
/>
);
}
export function LogoType({ size = "default" }: LogoTypeProps) {
return <OnyxLogoTypeIcon size={115} className="items-center w-full" />;
}

View File

@@ -1,248 +1,13 @@
import SvgSearch from "@/icons/search";
import React, { KeyboardEvent, ChangeEvent } from "react";
import { MagnifyingGlass } from "@phosphor-icons/react";
interface FullSearchBarProps {
disabled: boolean;
query: string;
setQuery: (query: string) => void;
onSearch: (fast?: boolean) => void;
agentic?: boolean;
toggleAgentic?: () => void;
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSetSummary[];
filterManager: any; // You might want to replace 'any' with a more specific type
finalAvailableDocumentSets: DocumentSetSummary[];
finalAvailableSources: string[];
tags: Tag[];
showingSidebar: boolean;
}
import {
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useRef } from "react";
import { SendIcon } from "../icons/icons";
import { Separator } from "@/components/ui/separator";
import KeyboardSymbol from "@/lib/browserUtilities";
import { CCPairBasicInfo, DocumentSetSummary, Tag } from "@/lib/types";
import { HorizontalSourceSelector } from "./filtering/HorizontalSourceSelector";
export const AnimatedToggle = ({
isOn,
handleToggle,
direction = "top",
}: {
isOn: boolean;
handleToggle: () => void;
direction?: "bottom" | "top";
}) => {
const commandSymbol = KeyboardSymbol();
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
ref={containerRef}
className="my-auto ml-auto flex justify-end items-center cursor-pointer"
onClick={handleToggle}
>
<div ref={contentRef} className="flex items-center">
<div
className={`
w-10 h-6 flex items-center rounded-full p-1 transition-all duration-300 ease-in-out
${isOn ? "bg-toggled-background" : "bg-untoggled-background"}
`}
>
<div
className={`
bg-white w-4 h-4 rounded-full shadow-md transform transition-all duration-300 ease-in-out
${isOn ? "translate-x-4" : ""}
`}
></div>
</div>
<p className="ml-2 text-sm">Pro</p>
</div>
</div>
</TooltipTrigger>
<TooltipContent side={direction} backgroundColor="bg-background-200">
<div className="bg-white my-auto p-6 rounded-lg max-w-sm">
<h2 className="text-xl text-text-800 font-bold mb-2">
Agentic Search
</h2>
<p className="text-text-700 text-sm mb-4">
Our most powerful search, have an AI agent guide you to pinpoint
exactly what you&apos;re looking for.
</p>
<Separator />
<h2 className="text-xl text-text-800 font-bold mb-2">
Fast Search
</h2>
<p className="text-text-700 text-sm mb-4">
Get quality results immediately, best suited for instant access to
your documents.
</p>
<p className="mt-2 flex text-xs">Shortcut: ({commandSymbol}/)</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
export default AnimatedToggle;
export const FullSearchBar = ({
disabled,
showingSidebar,
query,
setQuery,
onSearch,
agentic,
toggleAgentic,
ccPairs,
documentSets,
filterManager,
finalAvailableDocumentSets,
finalAvailableSources,
tags,
}: FullSearchBarProps) => {
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const target = event.target;
setQuery(target.value);
// Resize the textarea to fit the content
target.style.height = "24px";
const newHeight = target.scrollHeight;
target.style.height = `${newHeight}px`;
};
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!(event.nativeEvent as any).isComposing
) {
event.preventDefault();
if (!disabled) {
onSearch(agentic);
}
}
};
return (
<div
className="
opacity-100
w-full
h-fit
flex
flex-col
border
border-border-medium
rounded-lg
bg-background-chatbar
[&:has(textarea:focus)]::ring-1
[&:has(textarea:focus)]::ring-black
text-text-chatbar
"
>
<textarea
rows={3}
onKeyDownCapture={handleKeyDown}
className={`
m-0
w-full
shrink
resize-none
border-0
bg-background-chatbar
whitespace-normal
rounded-lg
break-word
overscroll-contain
outline-none
placeholder-subtle
resize-none
pl-4
pr-12
max-h-[6em]
py-4
h-14
placeholder:text-text-chatbar-subtle
`}
autoFocus
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder="Search for anything..."
value={query}
onChange={handleChange}
onKeyDown={(event) => {}}
suppressContentEditableWarning={true}
/>
<div
className={`flex flex-nowrap ${
showingSidebar ? " 2xl:justify-between" : "2xl:justify-end"
} justify-between 4xl:justify-end w-full max-w-full items-center space-x-3 py-3 px-4`}
>
<div
className={`-my-1 flex-grow 4xl:hidden ${
!showingSidebar && "2xl:hidden"
}`}
>
{(ccPairs.length > 0 || documentSets.length > 0) && (
<HorizontalSourceSelector
isHorizontal
{...filterManager}
showDocSidebar={false}
availableDocumentSets={finalAvailableDocumentSets}
existingSources={finalAvailableSources}
availableTags={tags}
/>
)}
</div>
<div className="flex-shrink-0 flex items-center my-auto gap-x-3">
{toggleAgentic && (
<AnimatedToggle isOn={agentic!} handleToggle={toggleAgentic} />
)}
<div className="my-auto pl-2">
<button
disabled={disabled}
onClick={() => {
onSearch(agentic);
}}
className="flex my-auto cursor-pointer"
>
<SendIcon
size={22}
className={`text-neutral-50 dark:text-neutral-900 p-1 my-auto rounded-full ${
query
? "bg-neutral-900 dark:bg-neutral-50"
: "bg-neutral-500 dark:bg-neutral-400"
}`}
/>
</button>
</div>
</div>
</div>
<div className="absolute bottom-2.5 right-10"></div>
</div>
);
};
interface SearchBarProps {
query: string;
setQuery: (query: string) => void;
onSearch: () => void;
}
export const SearchBar = ({ query, setQuery, onSearch }: SearchBarProps) => {
export function SearchBar({ query, setQuery, onSearch }: SearchBarProps) {
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const target = event.target;
setQuery(target.value);
@@ -265,21 +30,19 @@ export const SearchBar = ({ query, setQuery, onSearch }: SearchBarProps) => {
};
return (
<div className="flex text-text-chatbar justify-center">
<div className="flex items-center w-full opacity-100 border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent bg-background-search">
<MagnifyingGlass className="text-text-darker" />
<textarea
autoFocus
className="flex-grow ml-2 h-6 placeholder:text-text-chatbar-subtle outline-none placeholder-default overflow-hidden whitespace-normal resize-none"
role="textarea"
aria-multiline
placeholder="Search..."
value={query}
onChange={handleChange}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
/>
</div>
<div className="flex items-center w-full border rounded-08 p-padding-button">
<SvgSearch className="h-[1.2rem] w-[1.2rem] stroke-text-04" />
<textarea
autoFocus
className={`flex items-center flex-grow px-padding-button outline-none overflow-hidden whitespace-normal resize-none bg-transparent leading-[1.5rem] h-[1.5rem]`}
role="textarea"
aria-multiline
placeholder="Search..."
value={query}
onChange={handleChange}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
/>
</div>
);
};
}

View File

@@ -1,17 +1,17 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { CombinedSettings } from "@/app/admin/settings/interfaces";
import { createContext, useEffect, useState } from "react";
export const SettingsContext = createContext<CombinedSettings | null>(null);
export interface SettingsProviderProps {
children: React.ReactNode | JSX.Element;
settings: CombinedSettings;
}
export function SettingsProvider({
children,
settings,
}: {
children: React.ReactNode | JSX.Element;
settings: CombinedSettings;
}) {
}: SettingsProviderProps) {
const [isMobile, setIsMobile] = useState<boolean | undefined>();
useEffect(() => {
@@ -30,3 +30,17 @@ export function SettingsProvider({
</SettingsContext.Provider>
);
}
export const SettingsContext = createContext<CombinedSettings | undefined>(
undefined
);
export function useSettingsContext() {
const context = useContext(SettingsContext);
if (context === undefined) {
throw new Error(
"useSettingsContext must be used within an SettingsProvider"
);
}
return context;
}

View File

@@ -1,423 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { ChatSession } from "@/app/chat/interfaces";
import { useState, useEffect, useContext, useRef, useCallback } from "react";
import {
deleteChatSession,
getChatRetentionInfo,
renameChatSession,
} from "@/app/chat/services/lib";
import { BasicSelectable } from "@/components/BasicClickable";
import Link from "next/link";
import {
FiArrowRight,
FiCheck,
FiEdit2,
FiMoreHorizontal,
FiShare2,
FiTrash,
FiX,
} from "react-icons/fi";
import { DefaultDropdownElement } from "@/components/Dropdown";
import { Popover } from "@/components/popover/Popover";
import { ShareChatSessionModal } from "@/app/chat/components/modal/ShareChatSessionModal";
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { DragHandle } from "@/components/table/DragHandle";
import { WarningCircle } from "@phosphor-icons/react";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { useChatContext } from "@/components/context/ChatContext";
import { removeChatFromFolder } from "@/app/chat/components/folders/FolderManagement";
export function ChatSessionDisplay({
chatSession,
search,
isSelected,
closeSidebar,
showShareModal,
showDeleteModal,
isDragging,
parentFolderName,
}: {
chatSession: ChatSession;
isSelected: boolean;
search?: boolean;
closeSidebar?: () => void;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
isDragging?: boolean;
parentFolderName?: string;
}) {
const router = useRouter();
const [isHovered, setIsHovered] = useState(false);
const [isRenamingChat, setIsRenamingChat] = useState(false);
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
const [chatName, setChatName] = useState(chatSession.name);
const settings = useContext(SettingsContext);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const chatSessionRef = useRef<HTMLDivElement>(null);
const [popoverOpen, setPopoverOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const renamingRef = useRef<HTMLDivElement>(null);
const { refreshChatSessions, refreshFolders } = useChatContext();
const isMobile = settings?.isMobile;
const handlePopoverOpenChange = useCallback(
(open: boolean) => {
setPopoverOpen(open);
if (!open) {
setIsDeleteModalOpen(false);
}
},
[isDeleteModalOpen]
);
const handleDeleteClick = useCallback(() => {
setIsDeleteModalOpen(true);
}, []);
const handleCancelDelete = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDeleteModalOpen(false);
setPopoverOpen(false);
}, []);
const handleConfirmDelete = useCallback(
async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (showDeleteModal) {
showDeleteModal(chatSession);
}
await deleteChatSession(chatSession.id);
await refreshChatSessions();
await refreshFolders();
setIsDeleteModalOpen(false);
setPopoverOpen(false);
},
[chatSession, showDeleteModal, refreshChatSessions, refreshFolders]
);
const handleMoveOutOfFolder = useCallback(async () => {
try {
if (chatSession.folder_id === null) return;
await removeChatFromFolder(chatSession.folder_id, chatSession.id);
await refreshChatSessions();
await refreshFolders();
setPopoverOpen(false);
} catch (e) {
console.error("Failed to move chat out of folder", e);
}
}, [
chatSession.folder_id,
chatSession.id,
refreshChatSessions,
refreshFolders,
]);
const onRename = useCallback(
async (e?: React.MouseEvent) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
const response = await renameChatSession(chatSession.id, chatName);
if (response.ok) {
setIsRenamingChat(false);
router.refresh();
} else {
alert("Failed to rename chat session");
}
},
[chatSession.id, chatName, router]
);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
renamingRef.current &&
!renamingRef.current.contains(event.target as Node) &&
isRenamingChat
) {
onRename();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isRenamingChat, onRename]);
if (!settings) {
return <></>;
}
const { daysUntilExpiration, showRetentionWarning } = getChatRetentionInfo(
chatSession,
settings?.settings
);
const handleDragStart = (event: React.DragEvent<HTMLAnchorElement>) => {
event.dataTransfer.setData(CHAT_SESSION_ID_KEY, chatSession.id.toString());
event.dataTransfer.setData(
FOLDER_ID_KEY,
chatSession.folder_id?.toString() || ""
);
};
const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
// Prevent default touch behavior
event.preventDefault();
// Create a custom event to mimic drag start
const customEvent = new Event("dragstart", { bubbles: true });
(customEvent as any).dataTransfer = new DataTransfer();
(customEvent as any).dataTransfer.setData(
CHAT_SESSION_ID_KEY,
chatSession.id.toString()
);
(customEvent as any).dataTransfer.setData(
FOLDER_ID_KEY,
chatSession.folder_id?.toString() || ""
);
// Dispatch the custom event
event.currentTarget.dispatchEvent(customEvent);
};
return (
<>
{isShareModalVisible && (
<ShareChatSessionModal
chatSessionId={chatSession.id}
existingSharedStatus={chatSession.shared_status}
onClose={() => setIsShareModalVisible(false)}
/>
)}
<div className="bg-transparent" ref={chatSessionRef}>
<Link
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
className="flex group items-center w-full relative"
key={chatSession.id}
onClick={() => {
if (settings?.isMobile && closeSidebar) {
closeSidebar();
}
}}
href={
search
? `/search?searchId=${chatSession.id}`
: `/chat?chatId=${chatSession.id}`
}
scroll={false}
draggable={!isMobile}
onDragStart={!isMobile ? handleDragStart : undefined}
>
<div
className={`${
isMobile ? "visible" : "invisible group-hover:visible"
} flex-none`}
onTouchStart={isMobile ? handleTouchStart : undefined}
>
<DragHandle size={16} className="w-3 ml-[4px] mr-[2px]" />
</div>
<BasicSelectable
padding="extra"
isHovered={isHovered}
isDragging={isDragging}
fullWidth
selected={isSelected}
removeColors={isRenamingChat}
>
<>
<div
className={`flex ${
isRenamingChat ? "-mr-2" : ""
} text-text-dark text-sm leading-normal relative gap-x-2`}
>
{isRenamingChat ? (
<div className="flex items-center w-full" ref={renamingRef}>
<div className="flex-grow mr-2">
<input
ref={inputRef}
value={chatName}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onChange={(e) => {
setChatName(e.target.value);
}}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
onRename();
event.preventDefault();
}
}}
className="w-full text-sm bg-transparent border-b border-text-darker outline-none"
/>
</div>
<div className="flex text-text-500 flex-none">
<button onClick={onRename} className="p-1">
<FiCheck size={14} />
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setChatName(chatSession.name);
setIsRenamingChat(false);
setPopoverOpen(false);
}}
className="p-1"
>
<FiX size={14} />
</button>
</div>
</div>
) : (
<p className="break-all font-normal overflow-hidden dark:text-[#D4D4D4] whitespace-nowrap w-full mr-3 relative">
{chatName || `Unnamed Chat`}
<span
className={`absolute right-0 top-0 h-full w-2 bg-gradient-to-r from-transparent
${
isSelected
? "to-background-chat-selected"
: isHovered
? "to-background-chat-hover"
: "to-background-sidebar"
} `}
/>
</p>
)}
{!isRenamingChat && (
<div className="ml-auto my-auto justify-end flex z-30">
{!showShareModal && showRetentionWarning && (
<CustomTooltip
line
content={
<p>
This chat will expire{" "}
{daysUntilExpiration < 1
? "today"
: `in ${daysUntilExpiration} day${
daysUntilExpiration !== 1 ? "s" : ""
}`}
</p>
}
>
<div className="mr-1 hover:bg-black/10 p-1 -m-1 rounded z-50">
<WarningCircle className="text-warning" />
</div>
</CustomTooltip>
)}
{(isHovered || popoverOpen) && (
<div>
<div
onClick={(e) => {
e.preventDefault();
setPopoverOpen(!popoverOpen);
}}
className="-my-1"
>
<Popover
open={popoverOpen}
onOpenChange={handlePopoverOpenChange}
content={
<div className="p-1 rounded">
<FiMoreHorizontal
onClick={() => setPopoverOpen(true)}
size={16}
/>
</div>
}
popover={
<div
className={`border border-border text-text-dark rounded-lg bg-background z-50 ${
isDeleteModalOpen ? "w-64" : "w-48"
}`}
>
{!isDeleteModalOpen ? (
<>
{showShareModal && (
<DefaultDropdownElement
name="Share"
icon={FiShare2}
onSelect={() =>
showShareModal(chatSession)
}
/>
)}
{!search && (
<DefaultDropdownElement
name="Rename"
icon={FiEdit2}
onSelect={() => setIsRenamingChat(true)}
/>
)}
{chatSession.folder_id !== null && (
<DefaultDropdownElement
name={`Move out of ${parentFolderName ?? "group"}?`}
icon={FiArrowRight}
onSelect={handleMoveOutOfFolder}
/>
)}
<DefaultDropdownElement
name="Delete"
icon={FiTrash}
onSelect={handleDeleteClick}
/>
</>
) : (
<div className="p-3">
<p className="text-sm mb-3">
Are you sure you want to delete this chat?
</p>
<div className="flex justify-center gap-2">
<button
className="px-3 py-1 text-sm bg-background-200 rounded"
onClick={handleCancelDelete}
>
Cancel
</button>
<button
className="px-3 py-1 text-sm bg-red-500 text-white rounded"
onClick={handleConfirmDelete}
>
Delete
</button>
</div>
</div>
)}
</div>
}
requiresContentPadding
sideOffset={6}
triggerMaxWidth
/>
</div>
</div>
)}
</div>
)}
</div>
</>
</BasicSelectable>
</Link>
</div>
</>
);
}

View File

@@ -1,431 +0,0 @@
"use client";
import React, {
ForwardedRef,
forwardRef,
useContext,
useCallback,
useEffect,
} from "react";
import Link from "next/link";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
import { useRouter, useSearchParams } from "next/navigation";
import { ChatSession } from "@/app/chat/interfaces";
import { Folder } from "@/app/chat/components/folders/interfaces";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import {
DocumentIcon2,
KnowledgeGroupIcon,
NewChatIcon,
} from "@/components/icons/icons";
import { PagesTab } from "./PagesTab";
import { pageType } from "./types";
import LogoWithText from "@/components/header/LogoWithText";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { DragEndEvent } from "@dnd-kit/core";
import { useAssistantsContext } from "@/components/context/AssistantsContext";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { buildChatUrl } from "@/app/chat/services/lib";
import { reorderPinnedAssistants } from "@/lib/assistants/updateAssistantPreferences";
import { useUser } from "@/components/user/UserProvider";
import { DragHandle } from "@/components/table/DragHandle";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { CircleX, PinIcon } from "lucide-react";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { TruncatedText } from "@/components/ui/truncatedText";
interface HistorySidebarProps {
liveAssistant?: MinimalPersonaSnapshot | null;
page: pageType;
existingChats?: ChatSession[];
currentChatSession?: ChatSession | null | undefined;
folders?: Folder[];
toggleSidebar?: () => void;
toggled?: boolean;
removeToggle?: () => void;
reset?: () => void;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
explicitlyUntoggle: () => void;
setShowAssistantsModal: (show: boolean) => void;
toggleChatSessionSearchModal?: () => void;
}
interface SortableAssistantProps {
assistant: MinimalPersonaSnapshot;
active: boolean;
onClick: () => void;
onPinAction: (e: React.MouseEvent) => void;
pinned?: boolean;
}
const SortableAssistant: React.FC<SortableAssistantProps> = ({
assistant,
active,
onClick,
onPinAction,
pinned = true,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: assistant.id === 0 ? "assistant-0" : assistant.id });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
...(isDragging ? { zIndex: 1000, position: "relative" as const } : {}),
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="flex items-center group"
>
<DragHandle
size={16}
className={`w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab ${
!pinned ? "opacity-0" : ""
}`}
/>
<div
data-testid={`assistant-[${assistant.id}]`}
onClick={(e) => {
e.preventDefault();
if (!isDragging) {
onClick();
}
}}
className={`cursor-pointer w-full group hover:bg-background-chat-hover ${
active ? "bg-accent-background-selected" : ""
} relative flex items-center gap-x-2 py-1 px-2 rounded-md`}
>
<AssistantIcon assistant={assistant} size={16} className="flex-none" />
<TruncatedText
className="text-base mr-4 text-left w-fit line-clamp-1 text-ellipsis text-black dark:text-[#D4D4D4]"
text={assistant.name}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation();
onPinAction(e);
}}
className="group-hover:block hidden absolute right-2"
>
{pinned ? (
<CircleX
size={16}
className="text-text-history-sidebar-button"
/>
) : (
<PinIcon
size={16}
className="text-text-history-sidebar-button"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent>
{pinned
? "Unpin this assistant from the sidebar"
: "Pin this assistant to the sidebar"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
);
};
export const HistorySidebar = React.memo(
forwardRef<HTMLDivElement, HistorySidebarProps>(
(
{
liveAssistant,
reset = () => null,
setShowAssistantsModal = () => null,
toggled,
page,
existingChats,
currentChatSession,
folders,
explicitlyUntoggle,
toggleSidebar,
removeToggle,
showShareModal,
toggleChatSessionSearchModal,
showDeleteModal,
},
ref: ForwardedRef<HTMLDivElement>
) => {
const searchParams = useSearchParams();
const router = useRouter();
const { user, toggleAssistantPinnedStatus } = useUser();
const { refreshAssistants, pinnedAssistants, setPinnedAssistants } =
useAssistantsContext();
const currentChatId = currentChatSession?.id;
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
setPinnedAssistants((prevAssistants: MinimalPersonaSnapshot[]) => {
const oldIndex = prevAssistants.findIndex(
(a: MinimalPersonaSnapshot) =>
(a.id === 0 ? "assistant-0" : a.id) === active.id
);
const newIndex = prevAssistants.findIndex(
(a: MinimalPersonaSnapshot) =>
(a.id === 0 ? "assistant-0" : a.id) === over?.id
);
const newOrder = arrayMove(prevAssistants, oldIndex, newIndex);
// Ensure we're sending the correct IDs to the API
const reorderedIds = newOrder.map(
(a: MinimalPersonaSnapshot) => a.id
);
reorderPinnedAssistants(reorderedIds);
return newOrder;
});
}
},
[setPinnedAssistants, reorderPinnedAssistants]
);
const combinedSettings = useContext(SettingsContext);
if (!combinedSettings) {
return null;
}
const handleNewChat = () => {
reset();
const newChatUrl = `/${page}`;
router.push(newChatUrl);
};
return (
<>
<div
ref={ref}
className={`
flex
flex-none
gap-y-4
bg-background-sidebar
w-full
border-r
dark:border-none
dark:text-[#D4D4D4]
dark:bg-[#000]
border-sidebar-border
flex
flex-col relative
h-screen
pt-2
transition-transform
`}
>
<div className="px-4 pl-2">
<LogoWithText
showArrow={true}
toggled={toggled}
page={page}
toggleSidebar={toggleSidebar}
explicitlyUntoggle={explicitlyUntoggle}
/>
</div>
{page == "chat" && (
<div className="px-4 px-1 -mx-2 gap-y-1 flex-col text-text-history-sidebar-button flex gap-x-1.5 items-center items-center">
<Link
className="w-full px-2 py-1 group rounded-md items-center hover:bg-accent-background-hovered cursor-pointer transition-all duration-150 flex gap-x-2"
href={`/${page}`}
onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
return;
}
if (handleNewChat) {
handleNewChat();
}
}}
>
<NewChatIcon size={20} className="flex-none" />
<p className="my-auto flex font-normal items-center ">
New Chat
</p>
</Link>
<Link
className="w-full px-2 py-1 rounded-md items-center hover:bg-hover cursor-pointer transition-all duration-150 flex gap-x-2"
href="/chat/my-documents"
>
<KnowledgeGroupIcon
size={20}
className="flex-none text-text-history-sidebar-button"
/>
<p className="my-auto flex font-normal items-center text-base">
My Documents
</p>
</Link>
{user?.preferences?.shortcut_enabled && (
<Link
className="w-full px-2 py-1 rounded-md items-center hover:bg-accent-background-hovered cursor-pointer transition-all duration-150 flex gap-x-2"
href="/chat/input-prompts"
>
<DocumentIcon2
size={20}
className="flex-none text-text-history-sidebar-button"
/>
<p className="my-auto flex font-normal items-center text-base">
Prompt Shortcuts
</p>
</Link>
)}
</div>
)}
<div className="h-full relative overflow-x-hidden overflow-y-auto">
<div className="flex px-4 font-normal text-sm gap-x-2 leading-normal text-text-500/80 dark:text-[#D4D4D4] items-center font-normal leading-normal">
Assistants
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis]}
>
<SortableContext
items={pinnedAssistants.map((a) =>
a.id === 0 ? "assistant-0" : a.id
)}
strategy={verticalListSortingStrategy}
>
<div className="flex px-0 mr-4 flex-col gap-y-1 mt-1">
{pinnedAssistants.map(
(assistant: MinimalPersonaSnapshot) => (
<SortableAssistant
key={
assistant.id === 0 ? "assistant-0" : assistant.id
}
assistant={assistant}
active={assistant.id === liveAssistant?.id}
onClick={() => {
router.push(
buildChatUrl(searchParams, null, assistant.id)
);
}}
onPinAction={async (e: React.MouseEvent) => {
e.stopPropagation();
await toggleAssistantPinnedStatus(
pinnedAssistants.map((a) => a.id),
assistant.id,
false
);
await refreshAssistants();
}}
/>
)
)}
</div>
</SortableContext>
</DndContext>
{!pinnedAssistants.some((a) => a.id === liveAssistant?.id) &&
liveAssistant &&
// filter out the default assistant
liveAssistant.id !== 0 && (
<div className="w-full mt-1 pr-4">
<SortableAssistant
pinned={false}
assistant={liveAssistant}
active={liveAssistant.id === liveAssistant?.id}
onClick={() => {
router.push(
buildChatUrl(searchParams, null, liveAssistant.id)
);
}}
onPinAction={async (e: React.MouseEvent) => {
e.stopPropagation();
await toggleAssistantPinnedStatus(
[...pinnedAssistants.map((a) => a.id)],
liveAssistant.id,
true
);
await refreshAssistants();
}}
/>
</div>
)}
<div className="w-full px-4">
<button
aria-label="Explore Assistants"
onClick={() => setShowAssistantsModal(true)}
className="w-full cursor-pointer text-base text-black dark:text-[#D4D4D4] hover:bg-background-chat-hover flex items-center gap-x-2 py-1 px-2 rounded-md"
>
Explore Assistants
</button>
</div>
<PagesTab
toggleChatSessionSearchModal={toggleChatSessionSearchModal}
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={removeToggle}
existingChats={existingChats}
currentChatId={currentChatId}
folders={folders}
/>
</div>
</div>
</>
);
}
)
);
HistorySidebar.displayName = "HistorySidebar";

View File

@@ -1,482 +0,0 @@
import { ChatSession } from "@/app/chat/interfaces";
import {
createFolder,
updateFolderName,
deleteFolder,
addChatToFolder,
updateFolderDisplayPriorities,
} from "@/app/chat/components/folders/FolderManagement";
import { Folder } from "@/app/chat/components/folders/interfaces";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { FiPlus, FiCheck, FiX } from "react-icons/fi";
import { FolderDropdown } from "@/app/chat/components/folders/FolderDropdown";
import { ChatSessionDisplay } from "./ChatSessionDisplay";
import { useState, useCallback, useRef, useContext, useEffect } from "react";
import { Caret } from "@/components/icons/icons";
import { groupSessionsByDateRange } from "@/app/chat/services/lib";
import React from "react";
import {
Tooltip,
TooltipProvider,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { Search } from "lucide-react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useChatContext } from "@/components/context/ChatContext";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
interface SortableFolderProps {
folder: Folder;
children: React.ReactNode;
currentChatId?: string;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
closeSidebar?: () => void;
onEdit: (folderId: number, newName: string) => void;
onDelete: (folderId: number) => void;
onDrop: (folderId: number, chatSessionId: string) => void;
index: number;
}
const SortableFolder: React.FC<SortableFolderProps> = (props) => {
const settings = useContext(SettingsContext);
const mobile = settings?.isMobile;
const [isDragging, setIsDragging] = useState(false);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging: isDraggingDndKit,
} = useSortable({
id: props.folder.folder_id?.toString() ?? "",
disabled: mobile,
});
const ref = useRef<HTMLDivElement>(null);
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 1000 : "auto",
position: isDragging ? "relative" : "static",
opacity: isDragging ? 0.6 : 1,
};
useEffect(() => {
setIsDragging(isDraggingDndKit);
}, [isDraggingDndKit]);
return (
<div
ref={setNodeRef}
className="pr-3 ml-4 overflow-visible flex items-start"
style={style}
{...attributes}
>
<FolderDropdown {...listeners} ref={ref} {...props} />
</div>
);
};
export function PagesTab({
existingChats,
currentChatId,
folders,
closeSidebar,
showShareModal,
showDeleteModal,
toggleChatSessionSearchModal,
}: {
existingChats?: ChatSession[];
currentChatId?: string;
folders?: Folder[];
toggleChatSessionSearchModal?: () => void;
closeSidebar?: () => void;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
}) {
const { setPopup, popup } = usePopup();
const router = useRouter();
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
const newFolderInputRef = useRef<HTMLInputElement>(null);
const { reorderFolders, refreshFolders, refreshChatSessions } =
useChatContext();
const handleEditFolder = useCallback(
async (folderId: number, newName: string) => {
try {
await updateFolderName(folderId, newName);
setPopup({
message: "Folder updated successfully",
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Failed to update folder:", error);
setPopup({
message: `Failed to update folder: ${(error as Error).message}`,
type: "error",
});
}
},
[router, setPopup, refreshChatSessions, refreshFolders]
);
const handleDeleteFolder = useCallback(
(folderId: number) => {
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(
async (e: React.FormEvent<HTMLDivElement>) => {
e.preventDefault();
const newFolderName = newFolderInputRef.current?.value;
if (newFolderName) {
try {
await createFolder(newFolderName);
await refreshFolders();
router.refresh();
setPopup({
message: "Folder created successfully",
type: "success",
});
} catch (error) {
console.error("Failed to create folder:", error);
setPopup({
message:
error instanceof Error
? error.message
: "Failed to create folder",
type: "error",
});
}
}
setIsCreatingFolder(false);
},
[router, setPopup, refreshFolders]
);
const existingChatsNotinFolders = existingChats?.filter(
(chat) =>
!folders?.some((folder) =>
folder.chat_sessions?.some((session) => session.id === chat.id)
)
);
const groupedChatSesssions = groupSessionsByDateRange(
existingChatsNotinFolders || []
);
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",
});
}
// await refreshChatSessions();
await refreshFolders();
},
[router, setPopup]
);
const [isDraggingSessionId, setIsDraggingSessionId] = useState<string | null>(
null
);
const renderChatSession = useCallback(
(
chat: ChatSession,
foldersExisting: boolean,
parentFolderName?: string
) => {
return (
<div
key={chat.id}
className="-ml-4 bg-transparent -mr-2"
draggable
style={{
touchAction: "none",
}}
onDragStart={(e) => {
setIsDraggingSessionId(chat.id);
e.dataTransfer.setData("text/plain", chat.id);
}}
onDragEnd={() => setIsDraggingSessionId(null)}
>
<ChatSessionDisplay
chatSession={chat}
isSelected={currentChatId === chat.id}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
closeSidebar={closeSidebar}
isDragging={isDraggingSessionId === chat.id}
parentFolderName={parentFolderName}
/>
</div>
);
},
[currentChatId, showShareModal, showDeleteModal, closeSidebar]
);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id && folders) {
const oldIndex = folders.findIndex(
(f) => f.folder_id?.toString() === active.id
);
const newIndex = folders.findIndex(
(f) => f.folder_id?.toString() === over?.id
);
if (oldIndex !== -1 && newIndex !== -1) {
const newOrder = arrayMove(folders, oldIndex, newIndex);
const displayPriorityMap = newOrder.reduce(
(acc, folder, index) => {
if (folder.folder_id !== undefined) {
acc[folder.folder_id] = index;
}
return acc;
},
{} as Record<number, number>
);
updateFolderDisplayPriorities(displayPriorityMap);
reorderFolders(displayPriorityMap);
}
}
},
[folders]
);
return (
<div className="flex flex-col gap-y-2 flex-grow">
{popup}
<div className="px-4 mt-2 group mr-2 bg-background-sidebar dark:bg-transparent z-20">
<div className="flex group justify-between text-sm gap-x-2 text-text-300/80 items-center font-normal leading-normal">
<p>Chats</p>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
className="my-auto mr-auto group-hover:opacity-100 opacity-0 transition duration-200 cursor-pointer gap-x-1 items-center text-black text-xs font-medium leading-normal mobile:hidden"
onClick={() => {
toggleChatSessionSearchModal?.();
}}
>
<Search
className="flex-none text-text-mobile-sidebar"
size={12}
/>
</button>
</TooltipTrigger>
<TooltipContent>Search Chats</TooltipContent>
</Tooltip>
</TooltipProvider>
<button
onClick={handleCreateFolder}
className="flex group-hover:opacity-100 opacity-0 transition duration-200 cursor-pointer gap-x-1 items-center text-black text-xs font-medium leading-normal"
>
<FiPlus size={12} className="flex-none" />
Create Group
</button>
</div>
</div>
{isCreatingFolder ? (
<div className="px-4">
<div className="flex overflow-visible items-center w-full text-text-500 rounded-md p-1 relative">
<Caret size={16} className="flex-none mr-1" />
<input
onKeyDown={(e) => {
if (e.key === "Enter") {
handleNewFolderSubmit(e);
}
}}
ref={newFolderInputRef}
type="text"
placeholder="Enter group name"
className="text-sm font-medium bg-transparent outline-none w-full pb-1 border-b border-background-500 transition-colors duration-200"
/>
<div className="flex -my-1">
<div
onClick={handleNewFolderSubmit}
className="cursor-pointer px-1"
>
<FiCheck size={14} />
</div>
<div
onClick={() => setIsCreatingFolder(false)}
className="cursor-pointer px-1"
>
<FiX size={14} />
</div>
</div>
</div>
</div>
) : (
<></>
)}
{folders && folders.length > 0 && (
<DndContext
modifiers={[restrictToVerticalAxis]}
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={folders.map((f) => f.folder_id?.toString() ?? "")}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2">
{folders
.sort(
(a, b) =>
(a.display_priority ?? 0) - (b.display_priority ?? 0)
)
.map((folder, index) => (
<SortableFolder
key={folder.folder_id}
folder={folder}
currentChatId={currentChatId}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
closeSidebar={closeSidebar}
onEdit={handleEditFolder}
onDelete={handleDeleteFolder}
onDrop={handleDrop}
index={index}
>
{folder.chat_sessions &&
folder.chat_sessions.map((chat) =>
renderChatSession(
chat,
folders != undefined && folders.length > 0,
folder.folder_name
)
)}
</SortableFolder>
))}
</div>
</SortableContext>
</DndContext>
)}
<div className="pl-4 pr-3">
{!isHistoryEmpty && (
<>
{Object.entries(groupedChatSesssions)
.filter(([groupName, chats]) => chats.length > 0)
.map(([groupName, chats], index) => (
<FolderDropdown
key={groupName}
folder={{
folder_name: groupName,
chat_sessions: chats,
display_priority: 0,
}}
currentChatId={currentChatId}
showShareModal={showShareModal}
closeSidebar={closeSidebar}
onEdit={handleEditFolder}
onDrop={handleDrop}
index={folders ? folders.length + index : index}
>
{chats.map((chat) =>
renderChatSession(
chat,
folders != undefined && folders.length > 0
)
)}
</FolderDropdown>
))}
</>
)}
{isHistoryEmpty && (!folders || folders.length === 0) && (
<p className="text-sm max-w-full mt-2 w-[250px]">
Try sending a message! Your chat history will appear here.
</p>
)}
</div>
</div>
);
}

View File

@@ -8,48 +8,19 @@ import React, {
} from "react";
import { createPortal } from "react-dom";
// Create a context for the tooltip group
const TooltipGroupContext = createContext<{
// Interfaces
export interface TooltipGroupContextType {
setGroupHovered: React.Dispatch<React.SetStateAction<boolean>>;
groupHovered: boolean;
hoverCountRef: React.MutableRefObject<boolean>;
}>({
setGroupHovered: () => {},
groupHovered: false,
hoverCountRef: { current: false },
});
}
export const TooltipGroup: React.FC<{
export interface TooltipGroupProps {
children: React.ReactNode;
gap?: string;
}> = ({ children, gap }) => {
const [groupHovered, setGroupHovered] = useState(false);
const hoverCountRef = useRef(false);
}
return (
<TooltipGroupContext.Provider
value={{ groupHovered, setGroupHovered, hoverCountRef }}
>
<div className={`inline-flex ${gap}`}>{children}</div>
</TooltipGroupContext.Provider>
);
};
export const CustomTooltip = ({
content,
children,
large,
light,
citation,
line,
medium,
wrap,
showTick = false,
delay = 300,
position = "bottom",
disabled = false,
className,
}: {
export interface CustomTooltipProps {
medium?: boolean;
content: string | ReactNode;
children: JSX.Element;
@@ -63,7 +34,43 @@ export const CustomTooltip = ({
position?: "top" | "bottom";
disabled?: boolean;
className?: string;
}) => {
}
// Create a context for the tooltip group
const TooltipGroupContext = createContext<TooltipGroupContextType>({
setGroupHovered: () => {},
groupHovered: false,
hoverCountRef: { current: false },
});
export function TooltipGroup({ children, gap }: TooltipGroupProps) {
const [groupHovered, setGroupHovered] = useState(false);
const hoverCountRef = useRef(false);
return (
<TooltipGroupContext.Provider
value={{ groupHovered, setGroupHovered, hoverCountRef }}
>
<div className={`inline-flex ${gap}`}>{children}</div>
</TooltipGroupContext.Provider>
);
}
export function CustomTooltip({
content,
children,
large,
light,
citation,
line,
medium,
wrap,
showTick = false,
delay = 300,
position = "bottom",
disabled = false,
className,
}: CustomTooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -133,7 +140,7 @@ export const CustomTooltip = ({
!disabled &&
createPortal(
<div
className={`fixed z-[1000] overflow-hidden rounded-md text-neutral-50
className={`fixed z-[1000] overflow-hidden rounded-md text-text-inverted-01
${className}
${citation ? "max-w-[350px]" : "max-w-40"} ${
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
@@ -141,8 +148,8 @@ export const CustomTooltip = ({
transform -translate-x-1/2 text-xs
${
light
? "bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-50"
: "bg-neutral-900 dark:bg-neutral-200 text-neutral-50 dark:text-neutral-900"
? "bg-background-neutral-02 text-text-01"
: "bg-background-neutral-inverted-01 text-text-inverted-01"
}
px-2 py-1.5 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2`}
style={{
@@ -157,8 +164,8 @@ export const CustomTooltip = ({
} left-1/2 transform -translate-x-1/2 rotate-45
${
light
? "bg-neutral-200 dark:bg-neutral-800"
: "bg-neutral-900 dark:bg-neutral-200"
? "bg-background-neutral-02"
: "bg-background-neutral-inverted-01"
}`}
/>
)}
@@ -183,4 +190,4 @@ export const CustomTooltip = ({
)}
</>
);
};
}

View File

@@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-800",
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,10 +1,16 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium ring-offset-[#fff] transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
@@ -95,13 +101,6 @@ const buttonVariants = cva(
}
);
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
@@ -110,59 +109,56 @@ export interface ButtonProps
tooltip?: string;
reverse?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
variant,
size = "sm",
asChild = false,
icon: Icon,
tooltip,
disabled,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button";
const button = (
<Comp
className={cn(
buttonVariants({
variant,
size,
className,
})
)}
ref={ref}
disabled={disabled}
{...props}
>
{Icon && <Icon />}
{props.children}
</Comp>
function ButtonInner(
{
className,
variant,
size = "sm",
asChild = false,
icon: Icon,
tooltip,
disabled,
...props
}: ButtonProps,
ref: any
) {
const Comp = asChild ? Slot : "button";
const button = (
<Comp
className={cn(
buttonVariants({
variant,
size,
className,
})
)}
ref={ref}
disabled={disabled}
{...props}
>
{Icon && <Icon />}
{props.children}
</Comp>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={disabled ? "cursor-not-allowed" : ""}>{button}</div>
</TooltipTrigger>
<TooltipContent showTick={true}>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={disabled ? "cursor-not-allowed" : ""}>
{button}
</div>
</TooltipTrigger>
<TooltipContent showTick={true}>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return button;
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
return button;
}
export const Button = React.forwardRef(ButtonInner);
Button.displayName = "Button";

View File

@@ -0,0 +1,29 @@
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"bg-background-neutral-00 p-spacing-inline z-50 rounded-12 min-w-[12.5rem] overflow-hidden border shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -19,29 +19,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 " +
"w-64 " +
"rounded-md " +
"border " +
"border-neutral-200 " +
"bg-white " +
"p-2 " +
"text-neutral-950 " +
"shadow-md " +
"outline-none " +
"data-[state=open]:animate-in " +
"data-[state=closed]:animate-out " +
"data-[state=closed]:fade-out-0 " +
"data-[state=open]:fade-in-0 " +
"data-[state=closed]:zoom-out-95 " +
"data-[state=open]:zoom-in-95 " +
"data-[side=bottom]:slide-in-from-top-2 " +
"data-[side=left]:slide-in-from-right-2 " +
"data-[side=right]:slide-in-from-left-2 " +
"data-[side=top]:slide-in-from-bottom-2 " +
"dark:border-neutral-800 " +
"dark:bg-neutral-950 " +
"dark:text-neutral-50",
"bg-background-neutral-00 p-spacing-inline z-50 rounded-12 min-w-[12.5rem] overflow-hidden border shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}

View File

@@ -54,13 +54,10 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
side={side}
className={cn(
`z-[100] overflow-hidden rounded-md text-neutral-50 ${
backgroundColor ||
"bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900"
}
${width || "max-w-40"}
`z-[100] rounded-md ${backgroundColor || "bg-background-tint-03"}
${width}
text-wrap
px-2 py-1.5 text-xs shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2`,
p-padding-button text-xs shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2`,
className
)}
{...props}

View File

@@ -5,40 +5,24 @@ import { User, UserRole } from "@/lib/types";
import { getCurrentUser } from "@/lib/user";
import { usePostHog } from "posthog-js/react";
import { CombinedSettings } from "@/app/admin/settings/interfaces";
import { SettingsContext } from "../settings/SettingsProvider";
import { useSettingsContext } from "@/components/settings/SettingsProvider";
import { useTokenRefresh } from "@/hooks/useTokenRefresh";
import { AuthTypeMetadata } from "@/lib/userSS";
interface UserContextType {
interface UserProviderProps {
authTypeMetadata: AuthTypeMetadata;
children: React.ReactNode;
user: User | null;
isAdmin: boolean;
isCurator: boolean;
refreshUser: () => Promise<void>;
isCloudSuperuser: boolean;
updateUserAutoScroll: (autoScroll: boolean) => Promise<void>;
updateUserShortcuts: (enabled: boolean) => Promise<void>;
toggleAssistantPinnedStatus: (
currentPinnedAssistantIDs: number[],
assistantId: number,
isPinned: boolean
) => Promise<boolean>;
updateUserTemperatureOverrideEnabled: (enabled: boolean) => Promise<void>;
settings: CombinedSettings;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export function UserProvider({
authTypeMetadata,
children,
user,
settings,
}: {
authTypeMetadata: AuthTypeMetadata;
children: React.ReactNode;
user: User | null;
settings: CombinedSettings;
}) {
const updatedSettings = useContext(SettingsContext);
}: UserProviderProps) {
const updatedSettings = useSettingsContext();
const posthog = usePostHog();
// For auto_scroll and temperature_override_enabled:
@@ -278,6 +262,24 @@ export function UserProvider({
);
}
interface UserContextType {
user: User | null;
isAdmin: boolean;
isCurator: boolean;
refreshUser: () => Promise<void>;
isCloudSuperuser: boolean;
updateUserAutoScroll: (autoScroll: boolean) => Promise<void>;
updateUserShortcuts: (enabled: boolean) => Promise<void>;
toggleAssistantPinnedStatus: (
currentPinnedAssistantIDs: number[],
assistantId: number,
isPinned: boolean
) => Promise<boolean>;
updateUserTemperatureOverrideEnabled: (enabled: boolean) => Promise<void>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export function useUser() {
const context = useContext(UserContext);
if (context === undefined) {

View File

@@ -0,0 +1,82 @@
import { useEffect, RefObject } from "react";
/**
* A generic hook that detects clicks outside of a referenced element.
*
* @param ref - A ref to the element to monitor for outside clicks
* @param callback - Function to call when a click outside is detected
* @param enabled - Whether the hook is enabled. Defaults to true.
*
* @example
* ```tsx
* const MyComponent = () => {
* const ref = useRef<HTMLDivElement>(null);
* const [isOpen, setIsOpen] = useState(false);
*
* useClickOutside(ref, () => setIsOpen(false), isOpen);
*
* return (
* <div ref={ref}>
* {isOpen && <div>Content</div>}
* </div>
* );
* };
* ```
*
* @example
* ```tsx
* const Dropdown = () => {
* const dropdownRef = useRef<HTMLDivElement>(null);
* const [isOpen, setIsOpen] = useState(false);
*
* useClickOutside(dropdownRef, () => setIsOpen(false), isOpen);
*
* return (
* <div>
* {isOpen && <div ref={dropdownRef}>Dropdown content</div>}
* </div>
* );
* };
* ```
*/
export function useClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
callback: () => void,
enabled: boolean
): void {
useEffect(() => {
if (!enabled) {
return;
}
const handleClickOutside = (event: Event) => {
const target = event.target as Node;
// Check if click is outside the main ref
if (ref.current && !ref.current.contains(target)) {
callback();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref, callback, enabled]);
}
/**
* A specialized version of useClickOutside for common modal/dropdown patterns.
*
* @param ref - A ref to the element to monitor for outside clicks
* @param callback - Function to call when a click outside is detected
* @param enabled - Whether the element is currently open/visible
*/
export function useClickOutsideWhenOpen<T extends HTMLElement>(
ref: RefObject<T>,
callback: () => void,
enabled: boolean
): void {
useClickOutside(ref, callback, enabled);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useEffect } from "react";
/**
* Custom hook that listens for the "Escape" key and calls the provided callback.
*
* @param callback - Function to call when the Escape key is pressed
* @param enabled - Optional boolean to enable/disable the hook (defaults to true)
*/
export function useEscape(callback: () => void, enabled: boolean = true) {
useEffect(() => {
if (!enabled) return;
function handleKeyDown(event: KeyboardEvent) {
if (event.key !== "Escape") return;
event.preventDefault();
callback();
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [callback, enabled]);
}

27
web/src/icons/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Compilation of SVGs into TypeScript React Components
## Manual Conversion Process
Integrating `@svgr/webpack` into the TypeScript compiler was not working via the recommended route (Next.js webpack configuration). The automatic SVG-to-React component conversion was causing compilation issues and import resolution problems.
Therefore, we need to manually convert each SVG into a TSX file using the following command:
```sh
bunx @svgr/cli ${SVG_FILE} --typescript --no-dimensions --svgo-config '{"plugins":[{"name":"removeAttrs","params":{"attrs":"stroke"}}]}' > ${SVG_FILE_NAME}.tsx
```
This command:
- Converts SVG files to TypeScript React components (`--typescript`)
- Removes width and height from the root SVG tag (`--no-dimensions`)
- Removes all `stroke` attributes from SVG elements (`--svgo-config` with `removeAttrs` plugin)
## Adding New SVGs
When adding a new SVG icon:
1. Place the SVG file in this directory (`web/src/icons/`)
2. Run the conversion command:
```sh
bunx @svgr/cli ${SVG_FILE} --typescript --no-dimensions --svgo-config '{"plugins":[{"name":"removeAttrs","params":{"attrs":"stroke"}}]}' > ${SVG_FILE_NAME}.tsx
```
3. Delete the original SVG file (keep only the generated `.tsx` file)

19
web/src/icons/actions.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgActions = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.06 6.24449L5.12 4.12225L3.06 2.00001M11.5501 14L14 11.5501M14 11.5501L11.5501 9.10017M14 11.5501H9.75552M4.12224 9.09889L6.24448 10.3242V12.7747L4.12224 14L2 12.7747V10.3242L4.12224 9.09889ZM14 4.12225C14 5.29433 13.0498 6.24449 11.8778 6.24449C10.7057 6.24449 9.75552 5.29433 9.75552 4.12225C9.75552 2.95017 10.7057 2.00001 11.8778 2.00001C13.0498 2.00001 14 2.95017 14 4.12225Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgActions;

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgActivity = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14.6667 8H12L9.99999 14L5.99999 2L3.99999 8H1.33333"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgActivity;

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgArrowUpRight = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M4.66667 11.3333L11 5M4.66667 4.66663H11.3333V11.3333"
strokeWidth={1.5}
strokeLinejoin="round"
/>
</svg>
);
export default SvgArrowUpRight;

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgBarChart = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 13.3333V6.66666M8 13.3333V2.66666M4 13.3333V9.33332"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgBarChart;

19
web/src/icons/bell.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgBell = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M9.15333 14C9.03613 14.2021 8.86789 14.3698 8.66548 14.4864C8.46307 14.6029 8.23359 14.6643 8 14.6643C7.76641 14.6643 7.53693 14.6029 7.33452 14.4864C7.1321 14.3698 6.96387 14.2021 6.84667 14M12 5.33334C12 4.27248 11.5786 3.25506 10.8284 2.50492C10.0783 1.75477 9.06087 1.33334 8 1.33334C6.93913 1.33334 5.92172 1.75477 5.17157 2.50492C4.42143 3.25506 4 4.27248 4 5.33334C4 10 2 11.3333 2 11.3333H14C14 11.3333 12 10 12 5.33334Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgBell;

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgBubbleText = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10.4939 6.5H5.5M8.00607 9.5H5.50607M1.5 13.5H10.5C12.7091 13.5 14.5 11.7091 14.5 9.5V6.5C14.5 4.29086 12.7091 2.5 10.5 2.5H5.5C3.29086 2.5 1.5 4.29086 1.5 6.5V13.5Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgBubbleText;

View File

@@ -0,0 +1,26 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgCheckCircle = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_16_2879)">
<path
d="M14.6667 7.38668V8.00001C14.6658 9.43763 14.2003 10.8365 13.3396 11.9879C12.4788 13.1393 11.2689 13.9817 9.89023 14.3893C8.51162 14.7969 7.03817 14.7479 5.68964 14.2497C4.34112 13.7515 3.18976 12.8307 2.4073 11.6247C1.62484 10.4187 1.25319 8.99205 1.34778 7.55755C1.44237 6.12305 1.99813 4.75756 2.93218 3.66473C3.86623 2.57189 5.12852 1.81027 6.53079 1.49344C7.93306 1.17662 9.40017 1.32157 10.7133 1.90668M14.6667 2.66668L8 9.34001L6 7.34001"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_16_2879">
<rect width={16} height={16} fill="white" />
</clipPath>
</defs>
</svg>
);
export default SvgCheckCircle;

26
web/src/icons/clock.tsx Normal file
View File

@@ -0,0 +1,26 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgClock = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_16_2605)">
<path
d="M7.99999 3.99999V7.99999L10.6667 9.33333M14.6667 7.99999C14.6667 11.6819 11.6819 14.6667 7.99999 14.6667C4.3181 14.6667 1.33333 11.6819 1.33333 7.99999C1.33333 4.3181 4.3181 1.33333 7.99999 1.33333C11.6819 1.33333 14.6667 4.3181 14.6667 7.99999Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_16_2605">
<rect width={16} height={16} fill="white" />
</clipPath>
</defs>
</svg>
);
export default SvgClock;

26
web/src/icons/cloud.tsx Normal file
View File

@@ -0,0 +1,26 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgCloud = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_170_23)">
<path
d="M12 6.66669H11.16C10.9106 5.70069 10.3952 4.82401 9.67243 4.13628C8.94966 3.44856 8.04848 2.97735 7.07128 2.7762C6.09409 2.57506 5.08007 2.65205 4.14444 2.99842C3.20881 3.34478 2.3891 3.94664 1.77844 4.73561C1.16778 5.52457 0.790662 6.469 0.689941 7.46159C0.589219 8.45417 0.76893 9.45511 1.20865 10.3507C1.64838 11.2462 2.33048 12.0005 3.17746 12.5277C4.02443 13.055 5.00232 13.3341 6 13.3334H12C12.8841 13.3334 13.7319 12.9822 14.357 12.357C14.9821 11.7319 15.3333 10.8841 15.3333 10C15.3333 9.11597 14.9821 8.26812 14.357 7.643C13.7319 7.01788 12.8841 6.66669 12 6.66669Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_170_23">
<rect width={16} height={16} fill="white" />
</clipPath>
</defs>
</svg>
);
export default SvgCloud;

19
web/src/icons/code.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgCode = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10.6667 12L14.6667 8L10.6667 4M5.33334 4L1.33334 8L5.33334 12"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgCode;

26
web/src/icons/cpu.tsx Normal file
View File

@@ -0,0 +1,26 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgCpu = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_16_2615)">
<path
d="M6.09091 1V2.90909M9.90909 1V2.90909M6.09091 13.0909V15M9.90909 13.0909V15M13.0909 6.09091H15M13.0909 9.27273H15M1 6.09091H2.90909M1 9.27273H2.90909M4.18182 2.90909H11.8182C12.5211 2.90909 13.0909 3.47891 13.0909 4.18182V11.8182C13.0909 12.5211 12.5211 13.0909 11.8182 13.0909H4.18182C3.47891 13.0909 2.90909 12.5211 2.90909 11.8182V4.18182C2.90909 3.47891 3.47891 2.90909 4.18182 2.90909ZM6 6H10V10H6V6Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_16_2615">
<rect width={16} height={16} fill="white" />
</clipPath>
</defs>
</svg>
);
export default SvgCpu;

19
web/src/icons/dev-kit.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgDevKit = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M2 5H14M2 5V14H14V5M2 5C2 4.67722 2.11475 4.36495 2.32376 4.11897L4.12423 2H11.8795L13.6766 4.11869C13.8854 4.36487 14 4.67719 14 5M9.66666 11.1733L11.3333 9.50667L9.66666 7.84M6.33333 7.84L4.66666 9.50667L6.33333 11.1733"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgDevKit;

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgEditBig = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8 2.5H4C3.17157 2.5 2.5 3.17157 2.5 4V12C2.5 12.8284 3.17157 13.5 4 13.5H12C12.8284 13.5 13.5 12.8284 13.5 12V8M6 10V8.26485C6 8.08682 6.0707 7.91617 6.19654 7.79028L11.5938 2.3931C12.1179 1.86897 12.9677 1.86897 13.4918 2.3931L13.6069 2.50823C14.131 3.03236 14.131 3.88213 13.6069 4.40626L8.20971 9.80345C8.08389 9.92934 7.91317 10 7.73521 10H6Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinejoin="round"
/>
</svg>
);
export default SvgEditBig;

19
web/src/icons/edit.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgEdit = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8 13.3333H14M11 2.33334C11.2652 2.06813 11.6249 1.91913 12 1.91913C12.1857 1.91913 12.3696 1.95571 12.5412 2.02678C12.7128 2.09785 12.8687 2.20202 13 2.33334C13.1313 2.46466 13.2355 2.62057 13.3066 2.79215C13.3776 2.96373 13.4142 3.14762 13.4142 3.33334C13.4142 3.51906 13.3776 3.70296 13.3066 3.87454C13.2355 4.04612 13.1313 4.20202 13 4.33334L4.66667 12.6667L2 13.3333L2.66667 10.6667L11 2.33334Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgEdit;

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgFileText = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M9.66634 1.6665H2.99967C2.55765 1.6665 2.13372 1.8421 1.82116 2.15466C1.5086 2.46722 1.33301 2.89114 1.33301 3.33317V16.6665C1.33301 17.1085 1.5086 17.5325 1.82116 17.845C2.13372 18.1576 2.55765 18.3332 2.99967 18.3332H12.9997C13.4417 18.3332 13.8656 18.1576 14.1782 17.845C14.4907 17.5325 14.6663 17.1085 14.6663 16.6665V6.6665M9.66634 1.6665L14.6663 6.6665M9.66634 1.6665L9.66634 6.6665L14.6663 6.6665M11.333 10.8332H4.66634M11.333 14.1665H4.66634M6.33301 7.49984H4.66634"
strokeOpacity={0.4}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgFileText;

19
web/src/icons/filter.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgFilter = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14.6667 3H1.33334L6.66668 9.30667V12.6667L9.33334 14V9.30667L14.6667 3Z"
strokeOpacity={0.75}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgFilter;

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgFolderIn = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5 2.5L3 2.50001C2.17157 2.50001 1.5 3.17158 1.5 4.00001V12C1.5 12.8284 2.17157 13.5 3 13.5H13C13.8284 13.5 14.5 12.8284 14.5 12V6.00001C14.5 5.17158 13.8284 4.50001 13 4.50001L11 4.5M11 7.5L8.47141 10.0286C8.34124 10.1588 8.17062 10.2239 8.00001 10.2239M5.00001 7.5L7.52861 10.0286C7.65877 10.1588 7.82939 10.2239 8.00001 10.2239M7.99999 1.5L8.00001 10.2239"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgFolderIn;

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgFolderPlus = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M7.99999 7.33333V11.3333M5.99999 9.33333H10M14.6667 12.6667C14.6667 13.0203 14.5262 13.3594 14.2761 13.6095C14.0261 13.8595 13.6869 14 13.3333 14H2.66666C2.31304 14 1.9739 13.8595 1.72385 13.6095C1.4738 13.3594 1.33333 13.0203 1.33333 12.6667V3.33333C1.33333 2.97971 1.4738 2.64057 1.72385 2.39052C1.9739 2.14048 2.31304 2 2.66666 2H5.99999L7.33333 4H13.3333C13.6869 4 14.0261 4.14048 14.2761 4.39052C14.5262 4.64057 14.6667 4.97971 14.6667 5.33333V12.6667Z"
strokeOpacity={0.8}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgFolderPlus;

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