mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-01 13:45:44 +00:00
Compare commits
68 Commits
v2.12.0-cl
...
unified
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91eaf23162 | ||
|
|
bdafbfe0e8 | ||
|
|
278fd0e153 | ||
|
|
a4bb97bc22 | ||
|
|
8063d9a75e | ||
|
|
1ffaba12f0 | ||
|
|
26f8660663 | ||
|
|
d6504ed578 | ||
|
|
7fcc2c9d35 | ||
|
|
46e8f925fe | ||
|
|
5ec1f61839 | ||
|
|
df950963a7 | ||
|
|
93208a66ac | ||
|
|
a4819e07e7 | ||
|
|
f642ace40c | ||
|
|
9b430ae2d5 | ||
|
|
05f3f878b2 | ||
|
|
df17c5352e | ||
|
|
bcfb0f3cf3 | ||
|
|
38468c1dc4 | ||
|
|
8550a9c5e3 | ||
|
|
fe0c60e50d | ||
|
|
4ecc151a02 | ||
|
|
d08becead5 | ||
|
|
a429f852d5 | ||
|
|
a856f27fae | ||
|
|
d0d8027928 | ||
|
|
bd1671f1a1 | ||
|
|
e236c67678 | ||
|
|
683956697a | ||
|
|
fb1e303ffc | ||
|
|
729d4fafd1 | ||
|
|
40c60282d0 | ||
|
|
2141fd2c6e | ||
|
|
9aeba96043 | ||
|
|
b431de5141 | ||
|
|
d1a6340cfc | ||
|
|
ccf382ef4f | ||
|
|
c31997b9b2 | ||
|
|
ab31795a46 | ||
|
|
b3beca63dc | ||
|
|
cc6d54c1e6 | ||
|
|
ee12c0c5de | ||
|
|
d48912a05d | ||
|
|
c079072676 | ||
|
|
952f6bfb37 | ||
|
|
0714e4bb4e | ||
|
|
ae577f0f44 | ||
|
|
0705d584d8 | ||
|
|
36e391e557 | ||
|
|
1efce594b5 | ||
|
|
67ac53f17d | ||
|
|
d5a222925a | ||
|
|
d5ef928782 | ||
|
|
6963d78f8e | ||
|
|
d3ef2b8c17 | ||
|
|
70f4162ea8 | ||
|
|
883f52d332 | ||
|
|
f8fd83c883 | ||
|
|
d2bf0c0c5f | ||
|
|
5d598c2d22 | ||
|
|
9dc0e97302 | ||
|
|
048b2a6b39 | ||
|
|
7dd3cecf67 | ||
|
|
82abe28986 | ||
|
|
a0575e6a00 | ||
|
|
0c5bf5b3ed | ||
|
|
492117d910 |
87
web/package-lock.json
generated
87
web/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -311,6 +311,7 @@ export async function handleChatFeedback(
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function renameChatSession(
|
||||
chatSessionId: string,
|
||||
newName: string
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
143
web/src/components-2/Text.tsx
Normal file
143
web/src/components-2/Text.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
95
web/src/components-2/Truncated.tsx
Normal file
95
web/src/components-2/Truncated.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
web/src/components-2/buttons/Button.tsx
Normal file
46
web/src/components-2/buttons/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
web/src/components-2/buttons/MenuButton.tsx
Normal file
45
web/src/components-2/buttons/MenuButton.tsx
Normal 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>;
|
||||
}
|
||||
155
web/src/components-2/context/AgentsContext.tsx
Normal file
155
web/src/components-2/context/AgentsContext.tsx
Normal 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;
|
||||
}
|
||||
50
web/src/components-2/context/AppContext.tsx
Normal file
50
web/src/components-2/context/AppContext.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
96
web/src/components-2/context/AppSidebarContext.tsx
Normal file
96
web/src/components-2/context/AppSidebarContext.tsx
Normal 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;
|
||||
}
|
||||
53
web/src/components-2/context/ModalContext.tsx
Normal file
53
web/src/components-2/context/ModalContext.tsx
Normal 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;
|
||||
}
|
||||
59
web/src/components-2/modals/ConfirmationModal.tsx
Normal file
59
web/src/components-2/modals/ConfirmationModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
web/src/components-2/modals/CoreModal.tsx
Normal file
45
web/src/components-2/modals/CoreModal.tsx
Normal 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)!
|
||||
);
|
||||
}
|
||||
64
web/src/components-2/modals/Modal.tsx
Normal file
64
web/src/components-2/modals/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" : ""}`}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
"{model.query_prefix}"
|
||||
</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">
|
||||
"{model.passage_prefix}"
|
||||
</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">
|
||||
"{model.query_prefix}"
|
||||
</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">
|
||||
"{model.passage_prefix}"
|
||||
</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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -3152,6 +3152,7 @@ export const PinnedIcon = ({
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const OnyxLogoTypeIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 = ({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
50
web/src/components/ui/avatar.tsx
Normal file
50
web/src/components/ui/avatar.tsx
Normal 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 };
|
||||
@@ -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";
|
||||
|
||||
29
web/src/components/ui/hover-card.tsx
Normal file
29
web/src/components/ui/hover-card.tsx
Normal 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 };
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
82
web/src/hooks/useClickOutside.ts
Normal file
82
web/src/hooks/useClickOutside.ts
Normal 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);
|
||||
}
|
||||
27
web/src/hooks/useEscape.ts
Normal file
27
web/src/hooks/useEscape.ts
Normal 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
27
web/src/icons/README.md
Normal 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
19
web/src/icons/actions.tsx
Normal 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;
|
||||
19
web/src/icons/activity.tsx
Normal file
19
web/src/icons/activity.tsx
Normal 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;
|
||||
17
web/src/icons/arrow-up-right.tsx
Normal file
17
web/src/icons/arrow-up-right.tsx
Normal 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;
|
||||
18
web/src/icons/bar-chart.tsx
Normal file
18
web/src/icons/bar-chart.tsx
Normal 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
19
web/src/icons/bell.tsx
Normal 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;
|
||||
19
web/src/icons/bubble-text.tsx
Normal file
19
web/src/icons/bubble-text.tsx
Normal 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;
|
||||
26
web/src/icons/check-circle.tsx
Normal file
26
web/src/icons/check-circle.tsx
Normal 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
26
web/src/icons/clock.tsx
Normal 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
26
web/src/icons/cloud.tsx
Normal 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
19
web/src/icons/code.tsx
Normal 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
26
web/src/icons/cpu.tsx
Normal 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
19
web/src/icons/dev-kit.tsx
Normal 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;
|
||||
18
web/src/icons/edit-big.tsx
Normal file
18
web/src/icons/edit-big.tsx
Normal 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
19
web/src/icons/edit.tsx
Normal 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;
|
||||
19
web/src/icons/file-text.tsx
Normal file
19
web/src/icons/file-text.tsx
Normal 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
19
web/src/icons/filter.tsx
Normal 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;
|
||||
19
web/src/icons/folder-in.tsx
Normal file
19
web/src/icons/folder-in.tsx
Normal 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;
|
||||
19
web/src/icons/folder-plus.tsx
Normal file
19
web/src/icons/folder-plus.tsx
Normal 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
Reference in New Issue
Block a user