mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 13:19:16 +00:00
Merge branch 'features/big-upgrade-chat-ux' of github.com:khoj-ai/khoj into features/use-new-sse-in-new-chat-ux
This commit is contained in:
@@ -11,15 +11,69 @@ export interface UserProfile {
|
||||
detail: string;
|
||||
}
|
||||
|
||||
const userFetcher = () => window.fetch('/api/v1/user').then(res => res.json()).catch(err => console.log(err));
|
||||
const fetcher = (url: string) =>
|
||||
window.fetch(url)
|
||||
.then(res => res.json())
|
||||
.catch(err => console.warn(err));
|
||||
|
||||
export function useAuthenticatedData() {
|
||||
const { data, error } = useSWR<UserProfile>('/api/v1/user', fetcher, { revalidateOnFocus: false });
|
||||
|
||||
const { data, error } = useSWR<UserProfile>('/api/v1/user', userFetcher, { revalidateOnFocus: false });
|
||||
|
||||
if (error) return null;
|
||||
if (!data) return null;
|
||||
if (data.detail === 'Forbidden') return null;
|
||||
if (error || !data || data.detail === 'Forbidden') return null;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface ModelOptions {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
export interface SyncedContent {
|
||||
computer: boolean;
|
||||
github: boolean;
|
||||
notion: boolean;
|
||||
}
|
||||
export interface UserConfig {
|
||||
// user info
|
||||
username: string;
|
||||
user_photo: string | null;
|
||||
is_active: boolean;
|
||||
given_name: string;
|
||||
phone_number: string;
|
||||
is_phone_number_verified: boolean;
|
||||
// user content settings
|
||||
enabled_content_source: SyncedContent;
|
||||
has_documents: boolean;
|
||||
notion_token: string | null;
|
||||
// user model settings
|
||||
search_model_options: ModelOptions[];
|
||||
selected_search_model_config: number;
|
||||
chat_model_options: ModelOptions[];
|
||||
selected_chat_model_config: number;
|
||||
paint_model_options: ModelOptions[];
|
||||
selected_paint_model_config: number;
|
||||
voice_model_options: ModelOptions[];
|
||||
selected_voice_model_config: number;
|
||||
// user billing info
|
||||
subscription_state: string;
|
||||
subscription_renewal_date: string;
|
||||
// server settings
|
||||
khoj_cloud_subscription_url: string | undefined;
|
||||
billing_enabled: boolean;
|
||||
is_eleven_labs_enabled: boolean;
|
||||
is_twilio_enabled: boolean;
|
||||
khoj_version: string;
|
||||
anonymous_mode: boolean;
|
||||
notion_oauth_url: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
|
||||
export function useUserConfig(detailed: boolean = false) {
|
||||
const url = `/api/settings?detailed=${detailed}`;
|
||||
const { data, error } = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
|
||||
|
||||
if (error || !data || data.detail === 'Forbidden') return null;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface LocationData {
|
||||
|
||||
const locationFetcher = () => window.fetch("https://ipapi.co/json").then((res) => res.json()).catch((err) => console.log(err));
|
||||
|
||||
export const toTitleCase = (str: string) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
||||
|
||||
export function welcomeConsole() {
|
||||
console.log(`%c %s`, "font-family:monospace", `
|
||||
__ __ __ __ ______ __ _____ __
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import './globals.css';
|
||||
|
||||
import styles from './page.module.css';
|
||||
import React, { Suspense, useEffect, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import SuggestionCard from './components/suggestions/suggestionCard';
|
||||
import SidePanel from './components/sidePanel/chatHistorySidePanel';
|
||||
@@ -16,7 +16,7 @@ import 'katex/dist/katex.min.css';
|
||||
import ChatInputArea, { ChatOptions } from './components/chatInputArea/chatInputArea';
|
||||
import { useAuthenticatedData } from './common/auth';
|
||||
import { Card, CardTitle } from '@/components/ui/card';
|
||||
import { colorMap, convertColorToBorderClass } from './common/colorUtils';
|
||||
import { convertColorToBorderClass } from './common/colorUtils';
|
||||
import { getIconFromIconName } from './common/iconUtils';
|
||||
import { ClockCounterClockwise } from '@phosphor-icons/react';
|
||||
import { AgentData } from './agents/page';
|
||||
@@ -188,7 +188,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
||||
<div className={`ml-auto mr-auto ${props.isMobileWidth ? 'w-full' : 'w-fit'}`}>
|
||||
{
|
||||
!props.isMobileWidth &&
|
||||
<div className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center p-3 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none`}>
|
||||
<div className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none`}>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
|
||||
38
src/interface/web/app/settings/layout.tsx
Normal file
38
src/interface/web/app/settings/layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Noto_Sans } from "next/font/google";
|
||||
import "../globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Settings",
|
||||
description: "Configure Khoj to get personalized, deeper assistance.",
|
||||
icons: {
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
1010
src/interface/web/app/settings/page.tsx
Normal file
1010
src/interface/web/app/settings/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
11
src/interface/web/app/settings/settings.module.css
Normal file
11
src/interface/web/app/settings/settings.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
div.page {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
height: 100vh;
|
||||
color: hsla(var(--foreground));
|
||||
}
|
||||
div.contentBody {
|
||||
display: grid;
|
||||
margin: auto;
|
||||
}
|
||||
71
src/interface/web/components/ui/input-otp.tsx
Normal file
71
src/interface/web/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { Dot } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
117
src/interface/web/components/ui/table.tsx
Normal file
117
src/interface/web/components/ui/table.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -10,6 +10,10 @@ const nextConfig = {
|
||||
source: '/api/:path*',
|
||||
destination: 'http://localhost:42110/api/:path*',
|
||||
},
|
||||
{
|
||||
source: '/auth/:path*',
|
||||
destination: 'http://localhost:42110/auth/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
trailingSlash: true,
|
||||
|
||||
9216
src/interface/web/package-lock.json
generated
9216
src/interface/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,9 @@
|
||||
"dompurify": "^3.1.6",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"input-otp": "^1.2.4",
|
||||
"katex": "^0.16.10",
|
||||
"libphonenumber-js": "^1.11.4",
|
||||
"lucide-react": "^0.397.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-highlightjs": "^4.1.0",
|
||||
|
||||
@@ -89,10 +89,15 @@ const config = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2968,6 +2968,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
input-otp@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.2.4.tgz#9834af8675ac72c7f1b7c010f181b3b4ffdd0f72"
|
||||
integrity sha512-md6rhmD+zmMnUh5crQNSQxq3keBRYvE3odbr4Qb9g2NWzQv9azi+t1a3X4TBTbh98fsGHgEEJlzbe1q860uGCA==
|
||||
|
||||
internal-slot@^1.0.4, internal-slot@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
|
||||
@@ -3357,6 +3362,11 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
libphonenumber-js@^1.11.4:
|
||||
version "1.11.4"
|
||||
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz#e63fe553f45661b30bb10bb8c82c9cf2b22ec32a"
|
||||
integrity sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q==
|
||||
|
||||
lilconfig@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
|
||||
|
||||
Reference in New Issue
Block a user