Improved linking + scrolling (#3744)

* nits

* quick nit

* update various components

* quick nit

* update

* chat nits

* minor linear check fix
This commit is contained in:
pablonyx 2025-01-25 15:52:07 -08:00 committed by GitHub
parent b12c51f56c
commit d6863ec775
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 359 additions and 263 deletions

View File

@ -9,9 +9,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check PR body for Linear link or override - name: Check PR body for Linear link or override
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: | run: |
PR_BODY="${{ github.event.pull_request.body }}"
# Looking for "https://linear.app" in the body # Looking for "https://linear.app" in the body
if echo "$PR_BODY" | grep -qE "https://linear\.app"; then if echo "$PR_BODY" | grep -qE "https://linear\.app"; then
echo "Found a Linear link. Check passed." echo "Found a Linear link. Check passed."

View File

@ -890,47 +890,20 @@ export function AssistantEditor({
{imageGenerationTool && ( {imageGenerationTool && (
<> <>
<div className="flex items-center content-start mb-2"> <div className="flex items-center content-start mb-2">
<TooltipProvider> <BooleanFormField
<Tooltip> name={`enabled_tools_map.${imageGenerationTool.id}`}
<TooltipTrigger> label={imageGenerationTool.display_name}
<CheckboxField subtext="Generate and manipulate images using AI-powered tools"
size="sm" disabled={
id={`enabled_tools_map.${imageGenerationTool.id}`} !currentLLMSupportsImageOutput ||
name={`enabled_tools_map.${imageGenerationTool.id}`} !isImageGenerationAvailable
onCheckedChange={() => { }
if (isImageGenerationAvailable) { disabledTooltip={
toggleToolInValues( !currentLLMSupportsImageOutput
imageGenerationTool.id ? "To use Image Generation, select GPT-4 or another image compatible model as the default model for this Assistant."
); : "Image Generation requires an OpenAI or Azure Dall-E configuration."
} }
}} />
className={
!isImageGenerationAvailable
? "opacity-50 cursor-not-allowed"
: ""
}
/>
</TooltipTrigger>
{!isImageGenerationAvailable && (
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
{!currentLLMSupportsImageOutput
? "To use Image Generation, select GPT-4 or another image compatible model as the default model for this Assistant."
: "Image Generation requires an OpenAI or Azure Dalle configuration."}
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div className="flex flex-col ml-2">
<span className="text-sm">
{imageGenerationTool.display_name}
</span>
<span className="text-xs text-subtle">
Generate and manipulate images using AI-powered
tools
</span>
</div>
</div> </div>
</> </>
)} )}
@ -964,23 +937,12 @@ export function AssistantEditor({
{customTools.length > 0 && {customTools.length > 0 &&
customTools.map((tool) => ( customTools.map((tool) => (
<React.Fragment key={tool.id}> <BooleanFormField
<div className="flex items-center content-start mb-2"> key={tool.id}
<Checkbox name={`enabled_tools_map.${tool.id}`}
size="sm" label={tool.display_name}
id={`enabled_tools_map.${tool.id}`} subtext={tool.description}
checked={values.enabled_tools_map[tool.id]} />
onCheckedChange={() => {
toggleToolInValues(tool.id);
}}
/>
<div className="ml-2">
<span className="text-sm">
{tool.display_name}
</span>
</div>
</div>
</React.Fragment>
))} ))}
</div> </div>
</div> </div>
@ -1333,7 +1295,6 @@ export function AssistantEditor({
<BooleanFormField <BooleanFormField
small small
removeIndent removeIndent
alignTop
name="llm_relevance_filter" name="llm_relevance_filter"
label="AI Relevance Filter" label="AI Relevance Filter"
subtext="If enabled, the LLM will filter out documents that are not useful for answering the user query prior to generating a response. This typically improves the quality of the response but incurs slightly higher cost." subtext="If enabled, the LLM will filter out documents that are not useful for answering the user query prior to generating a response. This typically improves the quality of the response but incurs slightly higher cost."
@ -1342,7 +1303,6 @@ export function AssistantEditor({
<BooleanFormField <BooleanFormField
small small
removeIndent removeIndent
alignTop
name="include_citations" name="include_citations"
label="Citations" label="Citations"
subtext="Response will include citations ([1], [2], etc.) for documents referenced by the LLM. In general, we recommend to leave this enabled in order to increase trust in the LLM answer." subtext="Response will include citations ([1], [2], etc.) for documents referenced by the LLM. In general, we recommend to leave this enabled in order to increase trust in the LLM answer."
@ -1355,7 +1315,6 @@ export function AssistantEditor({
<BooleanFormField <BooleanFormField
small small
removeIndent removeIndent
alignTop
name="datetime_aware" name="datetime_aware"
label="Date and Time Aware" label="Date and Time Aware"
subtext='Toggle this option to let the assistant know the current date and time (formatted like: "Thursday Jan 1, 1970 00:01"). To inject it in a specific place in the prompt, use the pattern [[CURRENT_DATETIME]]' subtext='Toggle this option to let the assistant know the current date and time (formatted like: "Thursday Jan 1, 1970 00:01"). To inject it in a specific place in the prompt, use the pattern [[CURRENT_DATETIME]]'

View File

@ -50,7 +50,7 @@ export const rerankingModels: RerankingModel[] = [
cloud: true, cloud: true,
displayName: "LiteLLM", displayName: "LiteLLM",
description: "Host your own reranker or router with LiteLLM proxy", description: "Host your own reranker or router with LiteLLM proxy",
link: "https://docs.litellm.ai/docs/proxy", link: "https://docs.litellm.ai/docs/simple_proxy",
}, },
{ {
rerank_provider_type: null, rerank_provider_type: null,
@ -82,7 +82,7 @@ export const rerankingModels: RerankingModel[] = [
modelName: "rerank-english-v3.0", modelName: "rerank-english-v3.0",
displayName: "Cohere English", displayName: "Cohere English",
description: "High-performance English-focused reranking model.", description: "High-performance English-focused reranking model.",
link: "https://docs.cohere.com/docs/rerank", link: "https://docs.cohere.com/v2/reference/rerank",
}, },
{ {
cloud: true, cloud: true,
@ -90,7 +90,7 @@ export const rerankingModels: RerankingModel[] = [
modelName: "rerank-multilingual-v3.0", modelName: "rerank-multilingual-v3.0",
displayName: "Cohere Multilingual", displayName: "Cohere Multilingual",
description: "Powerful multilingual reranking model.", description: "Powerful multilingual reranking model.",
link: "https://docs.cohere.com/docs/rerank", link: "https://docs.cohere.com/v2/reference/rerank",
}, },
]; ];

View File

@ -1,14 +1,13 @@
"use client"; "use client";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { Persona } from "@/app/admin/assistants/interfaces";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Modal } from "@/components/Modal";
import AssistantCard from "./AssistantCard"; import AssistantCard from "./AssistantCard";
import { useAssistants } from "@/components/context/AssistantsContext"; import { useAssistants } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider"; import { useUser } from "@/components/user/UserProvider";
import { FilterIcon } from "lucide-react"; import { FilterIcon } from "lucide-react";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership"; import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { Dialog, DialogContent } from "@/components/ui/dialog";
export const AssistantBadgeSelector = ({ export const AssistantBadgeSelector = ({
text, text,
@ -21,11 +20,12 @@ export const AssistantBadgeSelector = ({
}) => { }) => {
return ( return (
<div <div
className={`${ className={`
selected select-none ${
? "bg-neutral-900 text-white" selected
: "bg-transparent text-neutral-900" ? "bg-neutral-900 text-white"
} 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`} : "bg-transparent text-neutral-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} onClick={toggleFilter}
> >
{text} {text}
@ -60,11 +60,15 @@ const useAssistantFilter = () => {
return { assistantFilters, toggleAssistantFilter, setAssistantFilters }; return { assistantFilters, toggleAssistantFilter, setAssistantFilters };
}; };
export default function AssistantModal({ interface AssistantModalProps {
hideModal,
}: {
hideModal: () => void; hideModal: () => void;
}) { modalHeight?: string;
}
export function AssistantModal({
hideModal,
modalHeight,
}: AssistantModalProps) {
const { assistants, pinnedAssistants } = useAssistants(); const { assistants, pinnedAssistants } = useAssistants();
const { assistantFilters, toggleAssistantFilter } = useAssistantFilter(); const { assistantFilters, toggleAssistantFilter } = useAssistantFilter();
const router = useRouter(); const router = useRouter();
@ -86,11 +90,11 @@ export default function AssistantModal({
!assistantFilters[AssistantFilter.Private] || !assistant.is_public; !assistantFilters[AssistantFilter.Private] || !assistant.is_public;
const pinnedFilter = const pinnedFilter =
!assistantFilters[AssistantFilter.Pinned] || !assistantFilters[AssistantFilter.Pinned] ||
(user?.preferences?.pinned_assistants?.includes(assistant.id) ?? false); (pinnedAssistants.map((a) => a.id).includes(assistant.id) ?? false);
const mineFilter = const mineFilter =
!assistantFilters[AssistantFilter.Mine] || !assistantFilters[AssistantFilter.Mine] ||
assistants.map((a: Persona) => checkUserOwnsAssistant(user, a)); checkUserOwnsAssistant(user, assistant);
return ( return (
(nameMatches || labelMatches) && (nameMatches || labelMatches) &&
@ -111,142 +115,145 @@ export default function AssistantModal({
(assistant) => !assistant.builtin_persona && !assistant.is_default_persona (assistant) => !assistant.builtin_persona && !assistant.is_default_persona
); );
const maxHeight = 900;
const calculatedHeight = Math.min(
Math.ceil(assistants.length / 2) * 170 + 200,
window.innerHeight * 0.8
);
const height = Math.min(calculatedHeight, maxHeight);
return ( return (
<Modal <Dialog open={true} onOpenChange={(open) => !open && hideModal()}>
heightOverride={`${height}px`} <DialogContent
onOutsideClick={hideModal} className="p-0 overflow-hidden max-h-[80vh] max-w-none w-[95%] bg-background rounded-sm shadow-2xl transform transition-all duration-300 ease-in-out relative w-11/12 max-w-4xl pt-10 pb-10 px-10 overflow-hidden flex flex-col max-w-4xl"
removeBottomPadding style={{
className={`max-w-4xl max-h-[90vh] ${height} w-[95%] overflow-hidden`} position: "fixed",
> top: "10vh",
<div className="flex flex-col h-full"> left: "50%",
<div className="flex bg-background flex-col sticky top-0 z-10"> transform: "translateX(-50%)",
<div className="flex px-2 justify-between items-center gap-x-2 mb-0"> margin: 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-[#dcdad4] flex items-center px-3"> >
{!isSearchFocused && ( <div className="flex overflow-hidden flex-col h-full">
<svg <div className="flex flex-col sticky top-0 z-10">
xmlns="http://www.w3.org/2000/svg" <div className="flex px-2 justify-between items-center gap-x-2 mb-0">
className="h-5 w-5 text-gray-400" <div className="h-12 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
fill="none" <div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-[#dcdad4] flex items-center px-3">
viewBox="0 0 24 24" {!isSearchFocused && (
stroke="currentColor" <svg
> xmlns="http://www.w3.org/2000/svg"
<path className="h-5 w-5 text-gray-400"
strokeLinecap="round" fill="none"
strokeLinejoin="round" viewBox="0 0 24 24"
strokeWidth={2} stroke="currentColor"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" >
/> <path
</svg> strokeLinecap="round"
)} strokeLinejoin="round"
<input strokeWidth={2}
value={searchQuery} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
onChange={(e) => setSearchQuery(e.target.value)} />
onFocus={() => setIsSearchFocused(true)} </svg>
onBlur={() => setIsSearchFocused(false)} )}
type="text" <input
className="w-full h-full bg-transparent outline-none text-black" value={searchQuery}
/> onChange={(e) => setSearchQuery(e.target.value)}
</div> onFocus={() => setIsSearchFocused(true)}
</div> onBlur={() => setIsSearchFocused(false)}
<button type="text"
onClick={() => router.push("/assistants/new")} className="w-full h-full bg-transparent outline-none text-black"
className="h-10 cursor-pointer px-6 py-3 bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-[#fffcf4] 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 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-neutral-200" />
</div>
<div className="flex-grow overflow-y-auto">
<h2 className="text-2xl font-semibold text-gray-800 mb-2 px-4 py-2">
Featured Assistants
</h2>
<div className="w-full px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{featuredAssistants.length > 0 ? (
featuredAssistants.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={pinnedAssistants
.map((a) => a.id)
.includes(assistant.id)}
persona={assistant}
closeModal={hideModal}
/> />
</div> </div>
))
) : (
<div className="col-span-2 text-center text-gray-500">
No featured assistants match filters
</div> </div>
)} <button
onClick={() => router.push("/assistants/new")}
className="h-10 cursor-pointer px-6 py-3 bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-[#fffcf4] 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 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-neutral-200" />
</div> </div>
{allAssistants && allAssistants.length > 0 && ( <div className="flex-grow overflow-y-auto">
<> <h2 className="text-2xl font-semibold text-gray-800 mb-2 px-4 py-2">
<h2 className="text-2xl font-semibold text-gray-800 mt-4 mb-2 px-4 py-2"> Featured Assistants
All Assistants </h2>
</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"> <div className="w-full px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{allAssistants {featuredAssistants.length > 0 ? (
.sort((a, b) => b.id - a.id) featuredAssistants.map((assistant, index) => (
.map((assistant, index) => ( <div key={index}>
<div key={index}> <AssistantCard
<AssistantCard pinned={pinnedAssistants
pinned={ .map((a) => a.id)
user?.preferences?.pinned_assistants?.includes( .includes(assistant.id)}
assistant.id persona={assistant}
) ?? false closeModal={hideModal}
} />
persona={assistant} </div>
closeModal={hideModal} ))
/> ) : (
</div> <div className="col-span-2 text-center text-gray-500">
))} No featured assistants match filters
</div> </div>
</> )}
)} </div>
{allAssistants && allAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-gray-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> </DialogContent>
</Modal> </Dialog>
); );
} }
export default AssistantModal;

View File

@ -694,6 +694,7 @@ export function ChatInputBar({
flexPriority="stiff" flexPriority="stiff"
name="Filters" name="Filters"
Icon={FiFilter} Icon={FiFilter}
toggle
tooltipContent="Filter your search" tooltipContent="Filter your search"
/> />
} }

View File

@ -22,7 +22,7 @@ export const MemoizedAnchor = memo(
const index = parseInt(match[1], 10) - 1; const index = parseInt(match[1], 10) - 1;
const associatedDoc = docs?.[index]; const associatedDoc = docs?.[index];
if (!associatedDoc) { if (!associatedDoc) {
return <>{children}</>; return <a href={children as string}>{children}</a>;
} }
let icon: React.ReactNode = null; let icon: React.ReactNode = null;
@ -77,9 +77,24 @@ export const MemoizedLink = memo((props: any) => {
); );
} }
const handleMouseDown = () => {
let url = rest.href || rest.children?.toString();
if (url && !url.startsWith("http://") && !url.startsWith("https://")) {
// Try to construct a valid URL
const httpsUrl = `https://${url}`;
try {
new URL(httpsUrl);
url = httpsUrl;
} catch {
// If not a valid URL, don't modify original url
}
}
window.open(url, "_blank");
};
return ( return (
<a <a
onMouseDown={() => rest.href && window.open(rest.href, "_blank")} onMouseDown={handleMouseDown}
className="cursor-pointer text-link hover:text-link-hover" className="cursor-pointer text-link hover:text-link-hover"
> >
{rest.children} {rest.children}

View File

@ -375,7 +375,11 @@ export const AIMessage = ({
return ( return (
<> <>
<div <div
style={{ position: "absolute", left: "-9999px" }} style={{
position: "absolute",
left: "-9999px",
display: "none",
}}
dangerouslySetInnerHTML={{ __html: htmlContent }} dangerouslySetInnerHTML={{ __html: htmlContent }}
/> />
<ReactMarkdown <ReactMarkdown

View File

@ -6,8 +6,17 @@ import React, {
useContext, useContext,
useState, useState,
useCallback, useCallback,
useLayoutEffect,
useRef,
} from "react"; } from "react";
import Link from "next/link"; import Link from "next/link";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { ChatSession } from "../interfaces"; import { ChatSession } from "../interfaces";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants"; import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
@ -44,6 +53,7 @@ import {
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { CircleX } from "lucide-react"; import { CircleX } from "lucide-react";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
interface HistorySidebarProps { interface HistorySidebarProps {
page: pageType; page: pageType;
@ -90,6 +100,24 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
...(isDragging ? { zIndex: 1000, position: "relative" as const } : {}), ...(isDragging ? { zIndex: 1000, position: "relative" as const } : {}),
}; };
const nameRef = useRef<HTMLParagraphElement>(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);
}, [assistant.name]);
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
@ -115,10 +143,28 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
: "" : ""
} relative flex items-center gap-x-2 py-1 px-2 rounded-md`} } relative flex items-center gap-x-2 py-1 px-2 rounded-md`}
> >
<AssistantIcon assistant={assistant} size={16} className="flex-none" /> <AssistantIcon assistant={assistant} size={20} className="flex-none" />
<p className="text-base text-left w-fit line-clamp-1 text-ellipsis text-black"> <TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<p
ref={nameRef}
className="text-base text-left w-fit line-clamp-1 text-ellipsis text-black"
>
{assistant.name}
</p>
</TooltipTrigger>
{isNameTruncated && (
<TooltipContent>{assistant.name}</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<span
ref={hiddenNameRef}
className="absolute left-[-9999px] whitespace-nowrap"
>
{assistant.name} {assistant.name}
</p> </span>
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -295,7 +341,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
</div> </div>
)} )}
<div className="h-full relative overflow-y-auto"> <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-[#6c6c6c]/80 items-center font-normal leading-normal"> <div className="flex px-4 font-normal text-sm gap-x-2 leading-normal text-[#6c6c6c]/80 items-center font-normal leading-normal">
Assistants Assistants
</div> </div>
@ -303,6 +349,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis]}
> >
<SortableContext <SortableContext
items={pinnedAssistants.map((a) => items={pinnedAssistants.map((a) =>

View File

@ -13,7 +13,7 @@ import { FiPlus, FiTrash2, FiCheck, FiX } from "react-icons/fi";
import { NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED } from "@/lib/constants"; import { NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED } from "@/lib/constants";
import { FolderDropdown } from "../folders/FolderDropdown"; import { FolderDropdown } from "../folders/FolderDropdown";
import { ChatSessionDisplay } from "./ChatSessionDisplay"; import { ChatSessionDisplay } from "./ChatSessionDisplay";
import { useState, useCallback, useRef, useContext } from "react"; import { useState, useCallback, useRef, useContext, useEffect } from "react";
import { Caret } from "@/components/icons/icons"; import { Caret } from "@/components/icons/icons";
import { groupSessionsByDateRange } from "../lib"; import { groupSessionsByDateRange } from "../lib";
import React from "react"; import React from "react";
@ -36,6 +36,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { useChatContext } from "@/components/context/ChatContext"; import { useChatContext } from "@/components/context/ChatContext";
import { SettingsContext } from "@/components/settings/SettingsProvider"; import { SettingsContext } from "@/components/settings/SettingsProvider";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
interface SortableFolderProps { interface SortableFolderProps {
folder: Folder; folder: Folder;
@ -53,34 +54,41 @@ interface SortableFolderProps {
const SortableFolder: React.FC<SortableFolderProps> = (props) => { const SortableFolder: React.FC<SortableFolderProps> = (props) => {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
const mobile = settings?.isMobile; const mobile = settings?.isMobile;
const { attributes, listeners, setNodeRef, transform, transition } = const [isDragging, setIsDragging] = useState(false);
useSortable({ const {
id: props.folder.folder_id?.toString() ?? "", attributes,
data: { listeners,
activationConstraint: { setNodeRef,
distance: 8, transform,
}, transition,
}, isDragging: isDraggingDndKit,
disabled: mobile, } = useSortable({
}); id: props.folder.folder_id?.toString() ?? "",
disabled: mobile,
});
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const style = {
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? 1000 : "auto",
position: isDragging ? "relative" : "static",
opacity: isDragging ? 0.6 : 1,
}; };
useEffect(() => {
setIsDragging(isDraggingDndKit);
}, [isDraggingDndKit]);
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
className="pr-3 ml-4 overflow-visible flex items-start" className="pr-3 ml-4 overflow-visible flex items-start"
style={style} style={style}
{...attributes}
{...listeners}
> >
<FolderDropdown <FolderDropdown ref={ref} {...props} />
ref={ref}
{...props}
{...(mobile ? {} : attributes)}
{...(mobile ? {} : listeners)}
/>
</div> </div>
); );
}; };
@ -359,6 +367,7 @@ export function PagesTab({
{folders && folders.length > 0 && ( {folders && folders.length > 0 && (
<DndContext <DndContext
modifiers={[restrictToVerticalAxis]}
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}

View File

@ -287,11 +287,53 @@
overflow-x: hidden; overflow-x: hidden;
} }
.scrollbar {
width: 100%;
height: 100%;
}
/* Styling for textarea scrollbar */
textarea::-webkit-scrollbar {
width: 8px;
}
textarea::-webkit-scrollbar-track {
background: var(--scrollbar-track);
border-radius: 4px;
}
textarea::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
textarea::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
/* Styling for textarea resize handle */
textarea {
resize: vertical;
}
/* For Firefox */
textarea {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.inputscroll::-webkit-scrollbar-track { .inputscroll::-webkit-scrollbar-track {
background: #e5e7eb; background: #e5e7eb;
scrollbar-width: none; scrollbar-width: none;
} }
::-webkit-scrollbar {
width: 0px;
/* Vertical scrollbar width */
height: 8px;
/* Horizontal scrollbar height */
}
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: transparent; background: transparent;
/* background: theme("colors.scrollbar.track"); */ /* background: theme("colors.scrollbar.track"); */

View File

@ -63,7 +63,7 @@ export function Modal({
<div <div
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
className={cn( className={cn(
`fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm h-full `fixed inset-0 bg-white bg-opacity-25 backdrop-blur-sm h-full
flex items-center justify-center z-[9999] transition-opacity duration-300 ease-in-out` flex items-center justify-center z-[9999] transition-opacity duration-300 ease-in-out`
)} )}
> >
@ -76,6 +76,7 @@ export function Modal({
}} }}
className={` className={`
bg-background bg-background
bg-blue-400
text-emphasis text-emphasis
rounded rounded
shadow-2xl shadow-2xl

View File

@ -25,11 +25,13 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { FaMarkdown } from "react-icons/fa"; import { FaMarkdown } from "react-icons/fa";
import { useRef, useState } from "react"; import { useRef, useState, useCallback } from "react";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { EditIcon } from "@/components/icons/icons"; import { EditIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { CheckboxField } from "@/components/ui/checkbox";
import { CheckedState } from "@radix-ui/react-checkbox";
export function SectionHeader({ export function SectionHeader({
children, children,
@ -51,7 +53,7 @@ export function Label({
return ( return (
<div <div
className={`block font-medium base ${className} ${ className={`block font-medium base ${className} ${
small ? "text-sm" : "text-base" small ? "text-xs" : "text-sm"
}`} }`}
> >
{children} {children}
@ -75,7 +77,7 @@ export function LabelWithTooltip({
} }
export function SubLabel({ children }: { children: string | JSX.Element }) { export function SubLabel({ children }: { children: string | JSX.Element }) {
return <div className="text-sm text-subtle mb-2">{children}</div>; return <div className="text-xs text-subtle">{children}</div>;
} }
export function ManualErrorMessage({ children }: { children: string }) { export function ManualErrorMessage({ children }: { children: string }) {
@ -439,53 +441,62 @@ interface BooleanFormFieldProps {
name: string; name: string;
label: string; label: string;
subtext?: string | JSX.Element; subtext?: string | JSX.Element;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
removeIndent?: boolean; removeIndent?: boolean;
small?: boolean; small?: boolean;
alignTop?: boolean;
noLabel?: boolean; noLabel?: boolean;
disabled?: boolean; disabled?: boolean;
checked?: boolean;
optional?: boolean; optional?: boolean;
tooltip?: string; tooltip?: string;
disabledTooltip?: string;
} }
export const BooleanFormField = ({ export const BooleanFormField = ({
name, name,
label, label,
subtext, subtext,
onChange,
removeIndent, removeIndent,
noLabel, noLabel,
optional, optional,
small, small,
disabled, disabled,
alignTop,
checked,
tooltip, tooltip,
disabledTooltip,
}: BooleanFormFieldProps) => { }: BooleanFormFieldProps) => {
const [field, meta, helpers] = useField<boolean>(name); const { setFieldValue } = useFormikContext<any>();
const { setValue } = helpers;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = useCallback(
setValue(e.target.checked); (checked: CheckedState) => {
if (onChange) { if (!disabled) {
onChange(e); setFieldValue(name, checked);
} }
}; },
[disabled, name, setFieldValue]
);
return ( return (
<div> <div>
<label className="flex text-sm"> <label className="flex items-center text-sm cursor-pointer">
<Field <TooltipProvider>
type="checkbox" <Tooltip>
{...field} <TooltipTrigger>
checked={checked !== undefined ? checked : field.value} <CheckboxField
disabled={disabled} name={name}
onChange={handleChange} size="sm"
className={`${removeIndent ? "mr-2" : "mx-3"} className={`
px-5 w-3.5 h-3.5 ${alignTop ? "mt-1" : "my-auto"}`} ${disabled ? "opacity-50" : ""}
/> ${removeIndent ? "mr-2" : "mx-3"}`}
onCheckedChange={handleChange}
/>
</TooltipTrigger>
{disabled && disabledTooltip && (
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
{disabledTooltip}
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{!noLabel && ( {!noLabel && (
<div> <div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">

View File

@ -139,7 +139,7 @@ export function AssistantIcon({
alt={assistant.name} alt={assistant.name}
src={buildImgUrl(assistant.uploaded_image_id)} src={buildImgUrl(assistant.uploaded_image_id)}
loading="lazy" loading="lazy"
className={`h-[${dimension}px] w-[${dimension}px] object-cover object-center rounded-sm transition-opacity duration-300 ${wrapperClass}`} className={`h-[${dimension}px] w-[${dimension}px] rounded-full object-cover object-center transition-opacity duration-300 ${wrapperClass}`}
style={style} style={style}
/> />
) : ( ) : (

View File

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}