mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-21 05:20:55 +02:00
Add ability to control available pages
This commit is contained in:
parent
15f7b42e2b
commit
a4869b727d
@ -76,6 +76,8 @@ from danswer.server.query_and_chat.query_backend import (
|
|||||||
admin_router as admin_query_router,
|
admin_router as admin_query_router,
|
||||||
)
|
)
|
||||||
from danswer.server.query_and_chat.query_backend import basic_router as query_router
|
from danswer.server.query_and_chat.query_backend import basic_router as query_router
|
||||||
|
from danswer.server.settings.api import admin_router as settings_admin_router
|
||||||
|
from danswer.server.settings.api import basic_router as settings_router
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
from danswer.utils.telemetry import optional_telemetry
|
from danswer.utils.telemetry import optional_telemetry
|
||||||
from danswer.utils.telemetry import RecordType
|
from danswer.utils.telemetry import RecordType
|
||||||
@ -279,6 +281,8 @@ def get_application() -> FastAPI:
|
|||||||
include_router_with_global_prefix_prepended(application, state_router)
|
include_router_with_global_prefix_prepended(application, state_router)
|
||||||
include_router_with_global_prefix_prepended(application, danswer_api_router)
|
include_router_with_global_prefix_prepended(application, danswer_api_router)
|
||||||
include_router_with_global_prefix_prepended(application, gpts_router)
|
include_router_with_global_prefix_prepended(application, gpts_router)
|
||||||
|
include_router_with_global_prefix_prepended(application, settings_router)
|
||||||
|
include_router_with_global_prefix_prepended(application, settings_admin_router)
|
||||||
|
|
||||||
if AUTH_TYPE == AuthType.DISABLED:
|
if AUTH_TYPE == AuthType.DISABLED:
|
||||||
# Server logs this during auth setup verification step
|
# Server logs this during auth setup verification step
|
||||||
|
30
backend/danswer/server/settings/api.py
Normal file
30
backend/danswer/server/settings/api.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from danswer.auth.users import current_admin_user
|
||||||
|
from danswer.auth.users import current_user
|
||||||
|
from danswer.db.models import User
|
||||||
|
from danswer.server.settings.models import Settings
|
||||||
|
from danswer.server.settings.store import load_settings
|
||||||
|
from danswer.server.settings.store import store_settings
|
||||||
|
|
||||||
|
|
||||||
|
admin_router = APIRouter(prefix="/admin/settings")
|
||||||
|
basic_router = APIRouter(prefix="/settings")
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.put("")
|
||||||
|
def put_settings(
|
||||||
|
settings: Settings, _: User | None = Depends(current_admin_user)
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
settings.check_validity()
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
store_settings(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@basic_router.get("")
|
||||||
|
def fetch_settings(_: User | None = Depends(current_user)) -> Settings:
|
||||||
|
return load_settings()
|
36
backend/danswer/server/settings/models.py
Normal file
36
backend/danswer/server/settings/models.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PageType(str, Enum):
|
||||||
|
CHAT = "chat"
|
||||||
|
SEARCH = "search"
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseModel):
|
||||||
|
"""General settings"""
|
||||||
|
|
||||||
|
chat_page_enabled: bool = True
|
||||||
|
search_page_enabled: bool = True
|
||||||
|
default_page: PageType = PageType.SEARCH
|
||||||
|
|
||||||
|
def check_validity(self) -> None:
|
||||||
|
chat_page_enabled = self.chat_page_enabled
|
||||||
|
search_page_enabled = self.search_page_enabled
|
||||||
|
default_page = self.default_page
|
||||||
|
|
||||||
|
if chat_page_enabled is False and search_page_enabled is False:
|
||||||
|
raise ValueError(
|
||||||
|
"One of `search_page_enabled` and `chat_page_enabled` must be True."
|
||||||
|
)
|
||||||
|
|
||||||
|
if default_page == PageType.CHAT and chat_page_enabled is False:
|
||||||
|
raise ValueError(
|
||||||
|
"The default page cannot be 'chat' if the chat page is disabled."
|
||||||
|
)
|
||||||
|
|
||||||
|
if default_page == PageType.SEARCH and search_page_enabled is False:
|
||||||
|
raise ValueError(
|
||||||
|
"The default page cannot be 'search' if the search page is disabled."
|
||||||
|
)
|
23
backend/danswer/server/settings/store.py
Normal file
23
backend/danswer/server/settings/store.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from danswer.dynamic_configs.factory import get_dynamic_config_store
|
||||||
|
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
||||||
|
from danswer.server.settings.models import Settings
|
||||||
|
|
||||||
|
|
||||||
|
_SETTINGS_KEY = "danswer_settings"
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings() -> Settings:
|
||||||
|
dynamic_config_store = get_dynamic_config_store()
|
||||||
|
try:
|
||||||
|
settings = Settings(**cast(dict, dynamic_config_store.load(_SETTINGS_KEY)))
|
||||||
|
except ConfigNotFoundError:
|
||||||
|
settings = Settings()
|
||||||
|
dynamic_config_store.store(_SETTINGS_KEY, settings.dict())
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
def store_settings(settings: Settings) -> None:
|
||||||
|
get_dynamic_config_store().store(_SETTINGS_KEY, settings.dict())
|
@ -24,13 +24,7 @@ const nextConfig = {
|
|||||||
// In production, something else (nginx in the one box setup) should take
|
// In production, something else (nginx in the one box setup) should take
|
||||||
// care of this redirect. TODO (chris): better support setups where
|
// care of this redirect. TODO (chris): better support setups where
|
||||||
// web_server and api_server are on different machines.
|
// web_server and api_server are on different machines.
|
||||||
const defaultRedirects = [
|
const defaultRedirects = [];
|
||||||
{
|
|
||||||
source: "/",
|
|
||||||
destination: "/search",
|
|
||||||
permanent: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") return defaultRedirects;
|
if (process.env.NODE_ENV === "production") return defaultRedirects;
|
||||||
|
|
||||||
|
149
web/src/app/admin/settings/SettingsForm.tsx
Normal file
149
web/src/app/admin/settings/SettingsForm.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Label, SubLabel } from "@/components/admin/connectors/Field";
|
||||||
|
import { Title } from "@tremor/react";
|
||||||
|
import { Settings } from "./interfaces";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { DefaultDropdown, Option } from "@/components/Dropdown";
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
label,
|
||||||
|
sublabel,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
sublabel: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex text-sm mb-4">
|
||||||
|
<input
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
type="checkbox"
|
||||||
|
className="mx-3 px-5 w-3.5 h-3.5 my-auto"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<SubLabel>{sublabel}</SubLabel>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Selector({
|
||||||
|
label,
|
||||||
|
subtext,
|
||||||
|
options,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
subtext: string;
|
||||||
|
options: Option<string>[];
|
||||||
|
selected: string;
|
||||||
|
onSelect: (value: string | number | null) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{label && <Label>{label}</Label>}
|
||||||
|
{subtext && <SubLabel>{subtext}</SubLabel>}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<DefaultDropdown
|
||||||
|
options={options}
|
||||||
|
selected={selected}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsForm({ settings }: { settings: Settings }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function updateSettingField(
|
||||||
|
updateRequests: { fieldName: keyof Settings; newValue: any }[]
|
||||||
|
) {
|
||||||
|
const newValues: any = {};
|
||||||
|
updateRequests.forEach(({ fieldName, newValue }) => {
|
||||||
|
newValues[fieldName] = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch("/api/admin/settings", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...settings,
|
||||||
|
...newValues,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
const errorMsg = (await response.json()).detail;
|
||||||
|
alert(`Failed to update settings. ${errorMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title className="mb-4">Page Visibility</Title>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Search Page Enabled?"
|
||||||
|
sublabel={`If set, then the "Search" page will be accessible to all users
|
||||||
|
and will show up as an option on the top navbar. If unset, then this
|
||||||
|
page will not be available.`}
|
||||||
|
checked={settings.search_page_enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updates: any[] = [
|
||||||
|
{ fieldName: "search_page_enabled", newValue: e.target.checked },
|
||||||
|
];
|
||||||
|
if (!e.target.checked && settings.default_page === "search") {
|
||||||
|
updates.push({ fieldName: "default_page", newValue: "chat" });
|
||||||
|
}
|
||||||
|
updateSettingField(updates);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Chat Page Enabled?"
|
||||||
|
sublabel={`If set, then the "Chat" page will be accessible to all users
|
||||||
|
and will show up as an option on the top navbar. If unset, then this
|
||||||
|
page will not be available.`}
|
||||||
|
checked={settings.chat_page_enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updates: any[] = [
|
||||||
|
{ fieldName: "chat_page_enabled", newValue: e.target.checked },
|
||||||
|
];
|
||||||
|
if (!e.target.checked && settings.default_page === "chat") {
|
||||||
|
updates.push({ fieldName: "default_page", newValue: "search" });
|
||||||
|
}
|
||||||
|
updateSettingField(updates);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Selector
|
||||||
|
label="Default Page"
|
||||||
|
subtext="The page that users will be redirected to after logging in. Can only be set to a page that is enabled."
|
||||||
|
options={[
|
||||||
|
{ value: "search", name: "Search" },
|
||||||
|
{ value: "chat", name: "Chat" },
|
||||||
|
]}
|
||||||
|
selected={settings.default_page}
|
||||||
|
onSelect={(value) => {
|
||||||
|
value &&
|
||||||
|
updateSettingField([
|
||||||
|
{ fieldName: "default_page", newValue: value },
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
5
web/src/app/admin/settings/interfaces.ts
Normal file
5
web/src/app/admin/settings/interfaces.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface Settings {
|
||||||
|
chat_page_enabled: boolean;
|
||||||
|
search_page_enabled: boolean;
|
||||||
|
default_page: "search" | "chat";
|
||||||
|
}
|
33
web/src/app/admin/settings/page.tsx
Normal file
33
web/src/app/admin/settings/page.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { FiSettings } from "react-icons/fi";
|
||||||
|
import { Settings } from "./interfaces";
|
||||||
|
import { fetchSS } from "@/lib/utilsSS";
|
||||||
|
import { SettingsForm } from "./SettingsForm";
|
||||||
|
import { Callout, Text } from "@tremor/react";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const response = await fetchSS("/settings");
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMsg = await response.text();
|
||||||
|
return <Callout title="Failed to fetch settings">{errorMsg}</Callout>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = (await response.json()) as Settings;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto container">
|
||||||
|
<AdminPageTitle
|
||||||
|
title="Workspace Settings"
|
||||||
|
icon={<FiSettings size={32} className="my-auto" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text className="mb-8">
|
||||||
|
Manage general Danswer settings applicable to all users in the
|
||||||
|
workspace.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<SettingsForm settings={settings} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,9 +9,11 @@ import { Persona } from "../admin/personas/interfaces";
|
|||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||||
|
import { Settings } from "../admin/settings/interfaces";
|
||||||
|
|
||||||
export function ChatLayout({
|
export function ChatLayout({
|
||||||
user,
|
user,
|
||||||
|
settings,
|
||||||
chatSessions,
|
chatSessions,
|
||||||
availableSources,
|
availableSources,
|
||||||
availableDocumentSets,
|
availableDocumentSets,
|
||||||
@ -21,6 +23,7 @@ export function ChatLayout({
|
|||||||
documentSidebarInitialWidth,
|
documentSidebarInitialWidth,
|
||||||
}: {
|
}: {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
settings: Settings | null;
|
||||||
chatSessions: ChatSession[];
|
chatSessions: ChatSession[];
|
||||||
availableSources: ValidSources[];
|
availableSources: ValidSources[];
|
||||||
availableDocumentSets: DocumentSet[];
|
availableDocumentSets: DocumentSet[];
|
||||||
@ -40,7 +43,7 @@ export function ChatLayout({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute top-0 z-40 w-full">
|
<div className="absolute top-0 z-40 w-full">
|
||||||
<Header user={user} />
|
<Header user={user} settings={settings} />
|
||||||
</div>
|
</div>
|
||||||
<HealthCheckBanner />
|
<HealthCheckBanner />
|
||||||
<InstantSSRAutoRefresh />
|
<InstantSSRAutoRefresh />
|
||||||
|
@ -27,6 +27,8 @@ import { personaComparator } from "../admin/personas/lib";
|
|||||||
import { ChatLayout } from "./ChatPage";
|
import { ChatLayout } from "./ChatPage";
|
||||||
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
|
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
|
||||||
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
||||||
|
import { getSettingsSS } from "@/lib/settings";
|
||||||
|
import { Settings } from "../admin/settings/interfaces";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
searchParams,
|
searchParams,
|
||||||
@ -43,6 +45,7 @@ export default async function Page({
|
|||||||
fetchSS("/persona?include_default=true"),
|
fetchSS("/persona?include_default=true"),
|
||||||
fetchSS("/chat/get-user-chat-sessions"),
|
fetchSS("/chat/get-user-chat-sessions"),
|
||||||
fetchSS("/query/valid-tags"),
|
fetchSS("/query/valid-tags"),
|
||||||
|
getSettingsSS(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// catch cases where the backend is completely unreachable here
|
// catch cases where the backend is completely unreachable here
|
||||||
@ -53,8 +56,9 @@ export default async function Page({
|
|||||||
| Response
|
| Response
|
||||||
| AuthTypeMetadata
|
| AuthTypeMetadata
|
||||||
| FullEmbeddingModelResponse
|
| FullEmbeddingModelResponse
|
||||||
|
| Settings
|
||||||
| null
|
| null
|
||||||
)[] = [null, null, null, null, null, null, null, null, null];
|
)[] = [null, null, null, null, null, null, null, null, null, null];
|
||||||
try {
|
try {
|
||||||
results = await Promise.all(tasks);
|
results = await Promise.all(tasks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -67,6 +71,7 @@ export default async function Page({
|
|||||||
const personasResponse = results[4] as Response | null;
|
const personasResponse = results[4] as Response | null;
|
||||||
const chatSessionsResponse = results[5] as Response | null;
|
const chatSessionsResponse = results[5] as Response | null;
|
||||||
const tagsResponse = results[6] as Response | null;
|
const tagsResponse = results[6] as Response | null;
|
||||||
|
const settings = results[7] as Settings | null;
|
||||||
|
|
||||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||||
if (!authDisabled && !user) {
|
if (!authDisabled && !user) {
|
||||||
@ -77,6 +82,10 @@ export default async function Page({
|
|||||||
return redirect("/auth/waiting-on-verification");
|
return redirect("/auth/waiting-on-verification");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings && !settings.chat_page_enabled) {
|
||||||
|
return redirect("/search");
|
||||||
|
}
|
||||||
|
|
||||||
let ccPairs: CCPairBasicInfo[] = [];
|
let ccPairs: CCPairBasicInfo[] = [];
|
||||||
if (ccPairsResponse?.ok) {
|
if (ccPairsResponse?.ok) {
|
||||||
ccPairs = await ccPairsResponse.json();
|
ccPairs = await ccPairsResponse.json();
|
||||||
@ -172,6 +181,7 @@ export default async function Page({
|
|||||||
|
|
||||||
<ChatLayout
|
<ChatLayout
|
||||||
user={user}
|
user={user}
|
||||||
|
settings={settings}
|
||||||
chatSessions={chatSessions}
|
chatSessions={chatSessions}
|
||||||
availableSources={availableSources}
|
availableSources={availableSources}
|
||||||
availableDocumentSets={documentSets}
|
availableDocumentSets={documentSets}
|
||||||
|
@ -9,6 +9,8 @@ import { redirect } from "next/navigation";
|
|||||||
import { BackendChatSession } from "../../interfaces";
|
import { BackendChatSession } from "../../interfaces";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { SharedChatDisplay } from "./SharedChatDisplay";
|
import { SharedChatDisplay } from "./SharedChatDisplay";
|
||||||
|
import { getSettingsSS } from "@/lib/settings";
|
||||||
|
import { Settings } from "@/app/admin/settings/interfaces";
|
||||||
|
|
||||||
async function getSharedChat(chatId: string) {
|
async function getSharedChat(chatId: string) {
|
||||||
const response = await fetchSS(
|
const response = await fetchSS(
|
||||||
@ -25,22 +27,13 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
|||||||
getAuthTypeMetadataSS(),
|
getAuthTypeMetadataSS(),
|
||||||
getCurrentUserSS(),
|
getCurrentUserSS(),
|
||||||
getSharedChat(params.chatId),
|
getSharedChat(params.chatId),
|
||||||
|
getSettingsSS(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// catch cases where the backend is completely unreachable here
|
// catch cases where the backend is completely unreachable here
|
||||||
// without try / catch, will just raise an exception and the page
|
// without try / catch, will just raise an exception and the page
|
||||||
// will not render
|
// will not render
|
||||||
let results: (User | AuthTypeMetadata | null)[] = [
|
let results: (User | AuthTypeMetadata | null)[] = [null, null, null, null];
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
];
|
|
||||||
try {
|
try {
|
||||||
results = await Promise.all(tasks);
|
results = await Promise.all(tasks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -49,6 +42,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
|||||||
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
||||||
const user = results[1] as User | null;
|
const user = results[1] as User | null;
|
||||||
const chatSession = results[2] as BackendChatSession | null;
|
const chatSession = results[2] as BackendChatSession | null;
|
||||||
|
const settings = results[3] as Settings | null;
|
||||||
|
|
||||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||||
if (!authDisabled && !user) {
|
if (!authDisabled && !user) {
|
||||||
@ -62,7 +56,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="absolute top-0 z-40 w-full">
|
<div className="absolute top-0 z-40 w-full">
|
||||||
<Header user={user} />
|
<Header user={user} settings={settings} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen">
|
<div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen">
|
||||||
|
16
web/src/app/page.tsx
Normal file
16
web/src/app/page.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { getSettingsSS } from "@/lib/settings";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const settings = await getSettingsSS();
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
redirect("/search");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.default_page === "search") {
|
||||||
|
redirect("/search");
|
||||||
|
} else {
|
||||||
|
redirect("/chat");
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,8 @@ import { personaComparator } from "../admin/personas/lib";
|
|||||||
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
|
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
|
||||||
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
|
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
|
||||||
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
||||||
|
import { getSettingsSS } from "@/lib/settings";
|
||||||
|
import { Settings } from "../admin/settings/interfaces";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
// Disable caching so we always get the up to date connector / document set / persona info
|
// Disable caching so we always get the up to date connector / document set / persona info
|
||||||
@ -38,6 +40,7 @@ export default async function Home() {
|
|||||||
fetchSS("/persona"),
|
fetchSS("/persona"),
|
||||||
fetchSS("/query/valid-tags"),
|
fetchSS("/query/valid-tags"),
|
||||||
fetchSS("/secondary-index/get-embedding-models"),
|
fetchSS("/secondary-index/get-embedding-models"),
|
||||||
|
getSettingsSS(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// catch cases where the backend is completely unreachable here
|
// catch cases where the backend is completely unreachable here
|
||||||
@ -48,6 +51,7 @@ export default async function Home() {
|
|||||||
| Response
|
| Response
|
||||||
| AuthTypeMetadata
|
| AuthTypeMetadata
|
||||||
| FullEmbeddingModelResponse
|
| FullEmbeddingModelResponse
|
||||||
|
| Settings
|
||||||
| null
|
| null
|
||||||
)[] = [null, null, null, null, null, null, null];
|
)[] = [null, null, null, null, null, null, null];
|
||||||
try {
|
try {
|
||||||
@ -62,6 +66,7 @@ export default async function Home() {
|
|||||||
const personaResponse = results[4] as Response | null;
|
const personaResponse = results[4] as Response | null;
|
||||||
const tagsResponse = results[5] as Response | null;
|
const tagsResponse = results[5] as Response | null;
|
||||||
const embeddingModelResponse = results[6] as Response | null;
|
const embeddingModelResponse = results[6] as Response | null;
|
||||||
|
const settings = results[7] as Settings | null;
|
||||||
|
|
||||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||||
if (!authDisabled && !user) {
|
if (!authDisabled && !user) {
|
||||||
@ -72,6 +77,10 @@ export default async function Home() {
|
|||||||
return redirect("/auth/waiting-on-verification");
|
return redirect("/auth/waiting-on-verification");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings && !settings.search_page_enabled) {
|
||||||
|
return redirect("/chat");
|
||||||
|
}
|
||||||
|
|
||||||
let ccPairs: CCPairBasicInfo[] = [];
|
let ccPairs: CCPairBasicInfo[] = [];
|
||||||
if (ccPairsResponse?.ok) {
|
if (ccPairsResponse?.ok) {
|
||||||
ccPairs = await ccPairsResponse.json();
|
ccPairs = await ccPairsResponse.json();
|
||||||
@ -143,7 +152,7 @@ export default async function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header user={user} />
|
<Header user={user} settings={settings} />
|
||||||
<div className="m-3">
|
<div className="m-3">
|
||||||
<HealthCheckBanner />
|
<HealthCheckBanner />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
|
import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
|
||||||
import { ChevronDownIcon } from "./icons/icons";
|
import { ChevronDownIcon } from "./icons/icons";
|
||||||
import { FiCheck, FiChevronDown } from "react-icons/fi";
|
import { FiCheck, FiChevronDown } from "react-icons/fi";
|
||||||
import { FaRobot } from "react-icons/fa";
|
|
||||||
|
|
||||||
export interface Option<T> {
|
export interface Option<T> {
|
||||||
name: string;
|
name: string;
|
||||||
@ -12,108 +11,6 @@ export interface Option<T> {
|
|||||||
|
|
||||||
export type StringOrNumberOption = Option<string | number>;
|
export type StringOrNumberOption = Option<string | number>;
|
||||||
|
|
||||||
interface DropdownProps<T> {
|
|
||||||
options: Option<T>[];
|
|
||||||
selected: string;
|
|
||||||
onSelect: (selected: Option<T> | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Dropdown = ({
|
|
||||||
options,
|
|
||||||
selected,
|
|
||||||
onSelect,
|
|
||||||
}: DropdownProps<string | number>) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const selectedName = options.find(
|
|
||||||
(option) => option.value === selected
|
|
||||||
)?.name;
|
|
||||||
|
|
||||||
const handleSelect = (option: StringOrNumberOption) => {
|
|
||||||
onSelect(option);
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`inline-flex
|
|
||||||
justify-center
|
|
||||||
w-full
|
|
||||||
px-4
|
|
||||||
py-3
|
|
||||||
text-sm
|
|
||||||
bg-gray-700
|
|
||||||
border
|
|
||||||
border-gray-300
|
|
||||||
rounded-md
|
|
||||||
shadow-sm
|
|
||||||
hover:bg-gray-700
|
|
||||||
focus:ring focus:ring-offset-0 focus:ring-1 focus:ring-offset-gray-800 focus:ring-blue-800
|
|
||||||
`}
|
|
||||||
id="options-menu"
|
|
||||||
aria-expanded="true"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
>
|
|
||||||
{selectedName ? <p>{selectedName}</p> : "Select an option..."}
|
|
||||||
<ChevronDownIcon className="text-gray-400 my-auto ml-auto" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOpen ? (
|
|
||||||
<div className="origin-top-right absolute left-0 mt-3 w-full rounded-md shadow-lg bg-gray-700 border-2 border-gray-600">
|
|
||||||
<div
|
|
||||||
role="menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="options-menu"
|
|
||||||
>
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => handleSelect(option)}
|
|
||||||
className={
|
|
||||||
`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800` +
|
|
||||||
(index !== 0 ? " border-t-2 border-gray-600" : "")
|
|
||||||
}
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<p className="font-medium">{option.name}</p>
|
|
||||||
{option.description && (
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-300">
|
|
||||||
{option.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function StandardDropdownOption<T>({
|
function StandardDropdownOption<T>({
|
||||||
index,
|
index,
|
||||||
option,
|
option,
|
||||||
|
@ -9,14 +9,15 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import { CustomDropdown, DefaultDropdownElement } from "./Dropdown";
|
import { CustomDropdown, DefaultDropdownElement } from "./Dropdown";
|
||||||
import { FiMessageSquare, FiSearch } from "react-icons/fi";
|
import { FiMessageSquare, FiSearch } from "react-icons/fi";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { Settings } from "@/app/admin/settings/interfaces";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
settings: Settings | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({ user }) => {
|
export function Header({ user, settings }: HeaderProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -56,7 +57,12 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
|
|||||||
return (
|
return (
|
||||||
<header className="border-b border-border bg-background-emphasis">
|
<header className="border-b border-border bg-background-emphasis">
|
||||||
<div className="mx-8 flex h-16">
|
<div className="mx-8 flex h-16">
|
||||||
<Link className="py-4" href="/search">
|
<Link
|
||||||
|
className="py-4"
|
||||||
|
href={
|
||||||
|
settings && settings.default_page === "chat" ? "/chat" : "/search"
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="h-[32px] w-[30px]">
|
<div className="h-[32px] w-[30px]">
|
||||||
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
|
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
|
||||||
@ -67,26 +73,31 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
{(!settings ||
|
||||||
href="/search"
|
(settings.search_page_enabled && settings.chat_page_enabled)) && (
|
||||||
className={"ml-6 h-full flex flex-col hover:bg-hover"}
|
<>
|
||||||
>
|
<Link
|
||||||
<div className="w-24 flex my-auto">
|
href="/search"
|
||||||
<div className={"mx-auto flex text-strong px-2"}>
|
className={"ml-6 h-full flex flex-col hover:bg-hover"}
|
||||||
<FiSearch className="my-auto mr-1" />
|
>
|
||||||
<h1 className="flex text-sm font-bold my-auto">Search</h1>
|
<div className="w-24 flex my-auto">
|
||||||
</div>
|
<div className={"mx-auto flex text-strong px-2"}>
|
||||||
</div>
|
<FiSearch className="my-auto mr-1" />
|
||||||
</Link>
|
<h1 className="flex text-sm font-bold my-auto">Search</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link href="/chat" className="h-full flex flex-col hover:bg-hover">
|
<Link href="/chat" className="h-full flex flex-col hover:bg-hover">
|
||||||
<div className="w-24 flex my-auto">
|
<div className="w-24 flex my-auto">
|
||||||
<div className="mx-auto flex text-strong px-2">
|
<div className="mx-auto flex text-strong px-2">
|
||||||
<FiMessageSquare className="my-auto mr-1" />
|
<FiMessageSquare className="my-auto mr-1" />
|
||||||
<h1 className="flex text-sm font-bold my-auto">Chat</h1>
|
<h1 className="flex text-sm font-bold my-auto">Chat</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="ml-auto h-full flex flex-col">
|
<div className="ml-auto h-full flex flex-col">
|
||||||
<div className="my-auto">
|
<div className="my-auto">
|
||||||
@ -124,7 +135,7 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Settings } from "@/app/admin/settings/interfaces";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
|
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
|
||||||
import {
|
import {
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
ConnectorIcon,
|
ConnectorIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
} from "@/components/icons/icons";
|
} from "@/components/icons/icons";
|
||||||
|
import { getSettingsSS } from "@/lib/settings";
|
||||||
import { User } from "@/lib/types";
|
import { User } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
AuthTypeMetadata,
|
AuthTypeMetadata,
|
||||||
@ -19,15 +21,21 @@ import {
|
|||||||
getCurrentUserSS,
|
getCurrentUserSS,
|
||||||
} from "@/lib/userSS";
|
} from "@/lib/userSS";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { FiCpu, FiLayers, FiPackage, FiSlack } from "react-icons/fi";
|
import {
|
||||||
|
FiCpu,
|
||||||
|
FiLayers,
|
||||||
|
FiPackage,
|
||||||
|
FiSettings,
|
||||||
|
FiSlack,
|
||||||
|
} from "react-icons/fi";
|
||||||
|
|
||||||
export async function Layout({ children }: { children: React.ReactNode }) {
|
export async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()];
|
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS(), getSettingsSS()];
|
||||||
|
|
||||||
// catch cases where the backend is completely unreachable here
|
// catch cases where the backend is completely unreachable here
|
||||||
// without try / catch, will just raise an exception and the page
|
// without try / catch, will just raise an exception and the page
|
||||||
// will not render
|
// will not render
|
||||||
let results: (User | AuthTypeMetadata | null)[] = [null, null];
|
let results: (User | AuthTypeMetadata | Settings | null)[] = [null, null];
|
||||||
try {
|
try {
|
||||||
results = await Promise.all(tasks);
|
results = await Promise.all(tasks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -36,6 +44,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
||||||
const user = results[1] as User | null;
|
const user = results[1] as User | null;
|
||||||
|
const settings = results[2] as Settings | null;
|
||||||
|
|
||||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||||
const requiresVerification = authTypeMetadata?.requiresVerification;
|
const requiresVerification = authTypeMetadata?.requiresVerification;
|
||||||
@ -54,7 +63,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div className="h-screen overflow-y-hidden">
|
<div className="h-screen overflow-y-hidden">
|
||||||
<div className="absolute top-0 z-50 w-full">
|
<div className="absolute top-0 z-50 w-full">
|
||||||
<Header user={user} />
|
<Header user={user} settings={settings} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full pt-16">
|
<div className="flex h-full pt-16">
|
||||||
<div className="w-80 pt-12 pb-8 h-full border-r border-border">
|
<div className="w-80 pt-12 pb-8 h-full border-r border-border">
|
||||||
@ -175,6 +184,20 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Settings",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<div className="flex">
|
||||||
|
<FiSettings size={18} />
|
||||||
|
<div className="ml-1">Workspace Settings</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
link: "/admin/settings",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
10
web/src/lib/settings.ts
Normal file
10
web/src/lib/settings.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Settings } from "@/app/admin/settings/interfaces";
|
||||||
|
import { buildUrl } from "./utilsSS";
|
||||||
|
|
||||||
|
export async function getSettingsSS(): Promise<Settings | null> {
|
||||||
|
const response = await fetch(buildUrl("/settings"));
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user