Forgot password feature (#3437)

* forgot password feature

* improved config

* nit

* nit
This commit is contained in:
pablonyx
2024-12-19 20:53:37 -08:00
committed by GitHub
parent 9011b8a139
commit aa4bfa2a78
21 changed files with 415 additions and 123 deletions

View File

@@ -66,6 +66,7 @@ jobs:
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_GTM_ENABLED=true
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true
# needed due to weird interactions with the builds for different platforms
no-cache: true
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -0,0 +1,80 @@
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from textwrap import dedent
from onyx.configs.app_configs import EMAIL_CONFIGURED
from onyx.configs.app_configs import EMAIL_FROM
from onyx.configs.app_configs import SMTP_PASS
from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.models import User
def send_email(
user_email: str,
subject: str,
body: str,
mail_from: str = EMAIL_FROM,
) -> None:
if not EMAIL_CONFIGURED:
raise ValueError("Email is not configured.")
msg = MIMEMultipart()
msg["Subject"] = subject
msg["To"] = user_email
if mail_from:
msg["From"] = mail_from
msg.attach(MIMEText(body))
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s:
s.starttls()
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg)
except Exception as e:
raise e
def send_user_email_invite(user_email: str, current_user: User) -> None:
subject = "Invitation to Join Onyx Workspace"
body = dedent(
f"""\
Hello,
You have been invited to join a workspace on Onyx.
To join the workspace, please visit the following link:
{WEB_DOMAIN}/auth/login
Best regards,
The Onyx Team
"""
)
send_email(user_email, subject, body, current_user.email)
def send_forgot_password_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
) -> None:
subject = "Onyx Forgot Password"
link = f"{WEB_DOMAIN}/auth/reset-password?token={token}"
body = f"Click the following link to reset your password: {link}"
send_email(user_email, subject, body, mail_from)
def send_user_verification_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
) -> None:
subject = "Onyx Email Verification"
link = f"{WEB_DOMAIN}/auth/verify-email?token={token}"
body = f"Click the following link to verify your email address: {link}"
send_email(user_email, subject, body, mail_from)

View File

@@ -1,10 +1,7 @@
import smtplib
import uuid
from collections.abc import AsyncGenerator
from datetime import datetime
from datetime import timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import cast
from typing import Dict
from typing import List
@@ -53,19 +50,17 @@ from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from onyx.auth.api_key import get_hashed_api_key_from_request
from onyx.auth.email_utils import send_forgot_password_email
from onyx.auth.email_utils import send_user_verification_email
from onyx.auth.invited_users import get_invited_users
from onyx.auth.schemas import UserCreate
from onyx.auth.schemas import UserRole
from onyx.auth.schemas import UserUpdate
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import DISABLE_AUTH
from onyx.configs.app_configs import EMAIL_FROM
from onyx.configs.app_configs import EMAIL_CONFIGURED
from onyx.configs.app_configs import REQUIRE_EMAIL_VERIFICATION
from onyx.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
from onyx.configs.app_configs import SMTP_PASS
from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import TRACK_EXTERNAL_IDP_EXPIRY
from onyx.configs.app_configs import USER_AUTH_SECRET
from onyx.configs.app_configs import VALID_EMAIL_DOMAINS
@@ -193,30 +188,6 @@ def verify_email_domain(email: str) -> None:
)
def send_user_verification_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
) -> None:
msg = MIMEMultipart()
msg["Subject"] = "Onyx Email Verification"
msg["To"] = user_email
if mail_from:
msg["From"] = mail_from
link = f"{WEB_DOMAIN}/auth/verify-email?token={token}"
body = MIMEText(f"Click the following link to verify your email address: {link}")
msg.attach(body)
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s:
s.starttls()
# If credentials fails with gmail, check (You need an app password, not just the basic email password)
# https://support.google.com/accounts/answer/185833?sjid=8512343437447396151-NA
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg)
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
reset_password_token_secret = USER_AUTH_SECRET
verification_token_secret = USER_AUTH_SECRET
@@ -506,7 +477,15 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
) -> None:
logger.notice(f"User {user.id} has forgot their password. Reset token: {token}")
if not EMAIL_CONFIGURED:
logger.error(
"Email is not configured. Please configure email in the admin panel"
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
"Your admin has not enbaled this feature.",
)
send_forgot_password_email(user.email, token)
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
@@ -624,9 +603,7 @@ def get_database_strategy(
auth_backend = AuthenticationBackend(
name="jwt" if MULTI_TENANT else "database",
transport=cookie_transport,
get_strategy=get_jwt_strategy if MULTI_TENANT else get_database_strategy, # type: ignore
name="jwt", transport=cookie_transport, get_strategy=get_jwt_strategy
) # type: ignore

View File

@@ -92,6 +92,7 @@ SMTP_SERVER = os.environ.get("SMTP_SERVER") or "smtp.gmail.com"
SMTP_PORT = int(os.environ.get("SMTP_PORT") or "587")
SMTP_USER = os.environ.get("SMTP_USER", "your-email@gmail.com")
SMTP_PASS = os.environ.get("SMTP_PASS", "your-gmail-password")
EMAIL_CONFIGURED = all([SMTP_SERVER, SMTP_USER, SMTP_PASS])
EMAIL_FROM = os.environ.get("EMAIL_FROM") or SMTP_USER
# If set, Onyx will listen to the `expires_at` returned by the identity

View File

@@ -21,6 +21,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from ee.onyx.configs.app_configs import SUPER_USERS
from onyx.auth.email_utils import send_user_email_invite
from onyx.auth.invited_users import get_invited_users
from onyx.auth.invited_users import write_invited_users
from onyx.auth.noauth_user import fetch_no_auth_user
@@ -61,7 +62,6 @@ from onyx.server.models import FullUserSnapshot
from onyx.server.models import InvitedUserSnapshot
from onyx.server.models import MinimalUserSnapshot
from onyx.server.utils import BasicAuthenticationError
from onyx.server.utils import send_user_email_invite
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
from shared_configs.configs import MULTI_TENANT

View File

@@ -1,21 +1,10 @@
import json
import smtplib
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from textwrap import dedent
from typing import Any
from fastapi import HTTPException
from fastapi import status
from onyx.configs.app_configs import SMTP_PASS
from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.models import User
class BasicAuthenticationError(HTTPException):
def __init__(self, detail: str):
@@ -62,31 +51,3 @@ def mask_credential_dict(credential_dict: dict[str, Any]) -> dict[str, str]:
masked_creds[key] = mask_string(val)
return masked_creds
def send_user_email_invite(user_email: str, current_user: User) -> None:
msg = MIMEMultipart()
msg["Subject"] = "Invitation to Join Onyx Workspace"
msg["From"] = current_user.email
msg["To"] = user_email
email_body = dedent(
f"""\
Hello,
You have been invited to join a workspace on Onyx.
To join the workspace, please visit the following link:
{WEB_DOMAIN}/auth/login
Best regards,
The Onyx Team
"""
)
msg.attach(MIMEText(email_body, "plain"))
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp_server:
smtp_server.starttls()
smtp_server.login(SMTP_USER, SMTP_PASS)
smtp_server.send_message(msg)

View File

@@ -267,7 +267,7 @@ services:
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
- NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN=${NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN:-}
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
# Enterprise Edition only
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
# DO NOT TURN ON unless you have EXPLICIT PERMISSION from Onyx.

View File

@@ -72,6 +72,7 @@ services:
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
depends_on:
- api_server
restart: always

View File

@@ -99,6 +99,7 @@ services:
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
depends_on:
- api_server
restart: always

View File

@@ -75,6 +75,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ARG NEXT_PUBLIC_GTM_ENABLED
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
RUN npx next build
# Step 2. Production image, copy all the files and run next
@@ -150,6 +153,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ARG NEXT_PUBLIC_GTM_ENABLED
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
# Note: Don't expose ports here, Compose will handle that for us if necessary.
# If you want to run this without compose, specify the ports to
# expose via cli

View File

@@ -81,13 +81,13 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
}
if (modelName?.toLowerCase().includes("phi")) {
return MicrosoftIconSVG;
}
}
if (modelName?.toLowerCase().includes("mistral")) {
return MistralIcon;
}
}
if (modelName?.toLowerCase().includes("llama")) {
return MetaIcon;
}
}
if (modelName?.toLowerCase().includes("gemini")) {
return GeminiIcon;
}

View File

@@ -0,0 +1,100 @@
"use client";
import React, { useState } from "react";
import { forgotPassword } from "./utils";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { TextFormField } from "@/components/admin/connectors/Field";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Spinner } from "@/components/Spinner";
import { redirect } from "next/navigation";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const ForgotPasswordPage: React.FC = () => {
const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false);
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
redirect("/auth/login");
}
return (
<AuthFlowContainer>
<div className="flex flex-col w-full justify-center">
<CardSection className="mt-4 w-full">
{" "}
<div className="flex">
<Title className="mb-2 mx-auto font-bold">Forgot Password</Title>
</div>
{isWorking && <Spinner />}
{popup}
<Formik
initialValues={{
email: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string().email().required(),
})}
onSubmit={async (values) => {
setIsWorking(true);
try {
await forgotPassword(values.email);
setPopup({
type: "success",
message:
"Password reset email sent. Please check your inbox.",
});
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: "An error occurred. Please try again.";
setPopup({
type: "error",
message: errorMessage,
});
} finally {
setIsWorking(false);
}
}}
>
{({ isSubmitting }) => (
<Form className="w-full flex flex-col items-stretch mt-2">
<TextFormField
name="email"
label="Email"
type="email"
placeholder="email@yourcompany.com"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
Reset Password
</Button>
</div>
</Form>
)}
</Formik>
<div className="flex">
<Text className="mt-4 mx-auto">
<Link href="/auth/login" className="text-link font-medium">
Back to Login
</Link>
</Text>
</div>
</CardSection>
</div>
</AuthFlowContainer>
);
};
export default ForgotPasswordPage;

View File

@@ -0,0 +1,33 @@
export const forgotPassword = async (email: string): Promise<void> => {
const response = await fetch(`/api/auth/forgot-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
const errorMessage =
error?.detail || "An error occurred during password reset.";
throw new Error(errorMessage);
}
};
export const resetPassword = async (
token: string,
password: string
): Promise<void> => {
const response = await fetch(`/api/auth/reset-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ token, password }),
});
if (!response.ok) {
throw new Error("Failed to reset password");
}
};

View File

@@ -10,6 +10,8 @@ import { requestEmailVerification } from "../lib";
import { useState } from "react";
import { Spinner } from "@/components/Spinner";
import { set } from "lodash";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import Link from "next/link";
export function EmailPasswordForm({
isSignup = false,
@@ -110,15 +112,21 @@ export function EmailPasswordForm({
placeholder="**************"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup && (
<Link
href="/auth/forgot-password"
className="text-sm text-link font-medium whitespace-nowrap"
>
{isSignup ? "Sign Up" : "Log In"}
</Button>
</div>
Forgot Password?
</Link>
)}
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
{isSignup ? "Sign Up" : "Log In"}
</Button>
</Form>
)}
</Formik>

View File

@@ -16,6 +16,7 @@ import { LoginText } from "./LoginText";
import { getSecondsUntilExpiration } from "@/lib/time";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const Page = async (props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -101,16 +102,24 @@ const Page = async (props: {
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
<div className="flex">
<Text className="mt-4 mx-auto">
Don&apos;t have an account?{" "}
<div className="flex mt-4 justify-between">
<Link
href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : ""
}`}
className="text-link font-medium"
>
Create an account
</Link>
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<Link
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
href="/auth/forgot-password"
className="text-link font-medium"
>
Create an account
Reset Password
</Link>
</Text>
)}
</div>
</div>
)}
@@ -123,11 +132,13 @@ const Page = async (props: {
</Title>
</div>
<EmailPasswordForm nextUrl={nextUrl} />
<div className="flex">
<Text className="mt-4 mx-auto">
<div className="flex flex-col gap-y-2 items-center">
<Text className="mt-4 ">
Don&apos;t have an account?{" "}
<Link
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : ""
}`}
className="text-link font-medium"
>
Create an account

View File

@@ -0,0 +1,117 @@
"use client";
import React, { useState } from "react";
import { resetPassword } from "../forgot-password/utils";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { TextFormField } from "@/components/admin/connectors/Field";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Spinner } from "@/components/Spinner";
import { redirect, useSearchParams } from "next/navigation";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const ResetPasswordPage: React.FC = () => {
const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false);
const searchParams = useSearchParams();
const token = searchParams.get("token");
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
redirect("/auth/login");
}
return (
<AuthFlowContainer>
<div className="flex flex-col w-full justify-center">
<CardSection className="mt-4 w-full">
<div className="flex">
<Title className="mb-2 mx-auto font-bold">Reset Password</Title>
</div>
{isWorking && <Spinner />}
{popup}
<Formik
initialValues={{
password: "",
confirmPassword: "",
}}
validationSchema={Yup.object().shape({
password: Yup.string().required("Password is required"),
confirmPassword: Yup.string()
.oneOf([Yup.ref("password"), undefined], "Passwords must match")
.required("Confirm Password is required"),
})}
onSubmit={async (values) => {
if (!token) {
setPopup({
type: "error",
message: "Invalid or missing reset token.",
});
return;
}
setIsWorking(true);
try {
await resetPassword(token, values.password);
setPopup({
type: "success",
message:
"Password reset successfully. Redirecting to login...",
});
setTimeout(() => {
redirect("/auth/login");
}, 1000);
} catch (error) {
setPopup({
type: "error",
message: "An error occurred. Please try again.",
});
} finally {
setIsWorking(false);
}
}}
>
{({ isSubmitting }) => (
<Form className="w-full flex flex-col items-stretch mt-2">
<TextFormField
name="password"
label="New Password"
type="password"
placeholder="Enter your new password"
/>
<TextFormField
name="confirmPassword"
label="Confirm New Password"
type="password"
placeholder="Confirm your new password"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
Reset Password
</Button>
</div>
</Form>
)}
</Formik>
<div className="flex">
<Text className="mt-4 mx-auto">
<Link href="/auth/login" className="text-link font-medium">
Back to Login
</Link>
</Text>
</div>
</CardSection>
</div>
</AuthFlowContainer>
);
};
export default ResetPasswordPage;

View File

@@ -211,7 +211,11 @@ export function TextFormField({
return (
<div className={`w-full ${width}`}>
<div className={`flex ${vertical ? "flex-col" : "flex-row"} items-start`}>
<div
className={`flex ${
vertical ? "flex-col" : "flex-row"
} gap-x-2 items-start`}
>
<div className="flex gap-x-2 items-center">
{!removeLabel && (
<Label className={sizeClass.label} small={small}>

View File

@@ -1121,12 +1121,16 @@ export const MetaIcon = ({
export const MicrosoftIconSVG = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => <LogoIcon size={size} className={className} src={microsoftSVG} />;
}: IconProps) => (
<LogoIcon size={size} className={className} src={microsoftSVG} />
);
export const MistralIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => <LogoIcon size={size} className={className} src={mistralSVG} />;
}: IconProps) => (
<LogoIcon size={size} className={className} src={mistralSVG} />
);
export const VoyageIcon = ({
size = 16,

View File

@@ -2,34 +2,15 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
isEditing?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, isEditing = true, style, ...props }, ref) => {
const textClassName = "text-2xl text-strong dark:text-neutral-50";
if (!isEditing) {
return (
<span className={cn(textClassName, className)}>
{props.value || props.defaultValue}
</span>
);
}
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
textClassName,
"w-[1ch] min-w-[1ch] box-content pr-1",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
style={{
width: `${Math.max(1, String(props.value || props.defaultValue || "").length)}ch`,
...style,
}}
ref={ref}
{...props}
/>

View File

@@ -75,6 +75,12 @@ export const NEXT_PUBLIC_CLOUD_ENABLED =
export const REGISTRATION_URL =
process.env.INTERNAL_URL || "http://127.0.0.1:3001";
export const SERVER_SIDE_ONLY__CLOUD_ENABLED =
process.env.NEXT_PUBLIC_CLOUD_ENABLED?.toLowerCase() === "true";
export const NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED =
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true";
export const TEST_ENV = process.env.TEST_ENV?.toLowerCase() === "true";
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =

View File

@@ -101,7 +101,7 @@ const MODEL_NAMES_SUPPORTING_IMAGE_INPUT = [
"amazon.nova-pro@v1",
// meta models
"llama-3.2-90b-vision-instruct",
"llama-3.2-11b-vision-instruct"
"llama-3.2-11b-vision-instruct",
];
export function checkLLMSupportsImageInput(model: string) {