diff --git a/.github/workflows/pr-linear-check.yml b/.github/workflows/pr-linear-check.yml index ac25b2e07..6bf0ce689 100644 --- a/.github/workflows/pr-linear-check.yml +++ b/.github/workflows/pr-linear-check.yml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Check PR body for Linear link or override + env: + PR_BODY: ${{ github.event.pull_request.body }} run: | - PR_BODY="${{ github.event.pull_request.body }}" - # Looking for "https://linear.app" in the body if echo "$PR_BODY" | grep -qE "https://linear\.app"; then echo "Found a Linear link. Check passed." diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx index 23fb03698..6692dbb5e 100644 --- a/web/src/app/admin/assistants/AssistantEditor.tsx +++ b/web/src/app/admin/assistants/AssistantEditor.tsx @@ -890,47 +890,20 @@ export function AssistantEditor({ {imageGenerationTool && ( <>
- - - - { - if (isImageGenerationAvailable) { - toggleToolInValues( - imageGenerationTool.id - ); - } - }} - className={ - !isImageGenerationAvailable - ? "opacity-50 cursor-not-allowed" - : "" - } - /> - - {!isImageGenerationAvailable && ( - -

- {!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."} -

-
- )} -
-
-
- - {imageGenerationTool.display_name} - - - Generate and manipulate images using AI-powered - tools - -
+
)} @@ -964,23 +937,12 @@ export function AssistantEditor({ {customTools.length > 0 && customTools.map((tool) => ( - -
- { - toggleToolInValues(tool.id); - }} - /> -
- - {tool.display_name} - -
-
-
+ ))} @@ -1333,7 +1295,6 @@ export function AssistantEditor({ { return (
{text} @@ -60,11 +60,15 @@ const useAssistantFilter = () => { return { assistantFilters, toggleAssistantFilter, setAssistantFilters }; }; -export default function AssistantModal({ - hideModal, -}: { +interface AssistantModalProps { hideModal: () => void; -}) { + modalHeight?: string; +} + +export function AssistantModal({ + hideModal, + modalHeight, +}: AssistantModalProps) { const { assistants, pinnedAssistants } = useAssistants(); const { assistantFilters, toggleAssistantFilter } = useAssistantFilter(); const router = useRouter(); @@ -86,11 +90,11 @@ export default function AssistantModal({ !assistantFilters[AssistantFilter.Private] || !assistant.is_public; const pinnedFilter = !assistantFilters[AssistantFilter.Pinned] || - (user?.preferences?.pinned_assistants?.includes(assistant.id) ?? false); + (pinnedAssistants.map((a) => a.id).includes(assistant.id) ?? false); const mineFilter = !assistantFilters[AssistantFilter.Mine] || - assistants.map((a: Persona) => checkUserOwnsAssistant(user, a)); + checkUserOwnsAssistant(user, assistant); return ( (nameMatches || labelMatches) && @@ -111,142 +115,145 @@ export default function AssistantModal({ (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 ( - -
-
-
-
-
- {!isSearchFocused && ( - - - - )} - setSearchQuery(e.target.value)} - onFocus={() => setIsSearchFocused(true)} - onBlur={() => setIsSearchFocused(false)} - type="text" - className="w-full h-full bg-transparent outline-none text-black" - /> -
-
- -
-
- - toggleAssistantFilter(AssistantFilter.Pinned)} - /> - - toggleAssistantFilter(AssistantFilter.Mine)} - /> - - toggleAssistantFilter(AssistantFilter.Private) - } - /> - toggleAssistantFilter(AssistantFilter.Public)} - /> -
-
-
- -
-

- Featured Assistants -

- -
- {featuredAssistants.length > 0 ? ( - featuredAssistants.map((assistant, index) => ( -
- a.id) - .includes(assistant.id)} - persona={assistant} - closeModal={hideModal} + !open && hideModal()}> + +
+
+
+
+
+ {!isSearchFocused && ( + + + + )} + setSearchQuery(e.target.value)} + onFocus={() => setIsSearchFocused(true)} + onBlur={() => setIsSearchFocused(false)} + type="text" + className="w-full h-full bg-transparent outline-none text-black" />
- )) - ) : ( -
- No featured assistants match filters
- )} + +
+
+ + + toggleAssistantFilter(AssistantFilter.Pinned) + } + /> + + toggleAssistantFilter(AssistantFilter.Mine)} + /> + + toggleAssistantFilter(AssistantFilter.Private) + } + /> + + toggleAssistantFilter(AssistantFilter.Public) + } + /> +
+
- {allAssistants && allAssistants.length > 0 && ( - <> -

- All Assistants -

+
+

+ Featured Assistants +

-
- {allAssistants - .sort((a, b) => b.id - a.id) - .map((assistant, index) => ( -
- -
- ))} -
- - )} +
+ {featuredAssistants.length > 0 ? ( + featuredAssistants.map((assistant, index) => ( +
+ a.id) + .includes(assistant.id)} + persona={assistant} + closeModal={hideModal} + /> +
+ )) + ) : ( +
+ No featured assistants match filters +
+ )} +
+ + {allAssistants && allAssistants.length > 0 && ( + <> +

+ All Assistants +

+ +
+ {allAssistants + .sort((a, b) => b.id - a.id) + .map((assistant, index) => ( +
+ +
+ ))} +
+ + )} +
-
- + +
); } +export default AssistantModal; diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index a59e8e30e..405831f23 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -694,6 +694,7 @@ export function ChatInputBar({ flexPriority="stiff" name="Filters" Icon={FiFilter} + toggle tooltipContent="Filter your search" /> } diff --git a/web/src/app/chat/message/MemoizedTextComponents.tsx b/web/src/app/chat/message/MemoizedTextComponents.tsx index c38c81820..66ba3f6df 100644 --- a/web/src/app/chat/message/MemoizedTextComponents.tsx +++ b/web/src/app/chat/message/MemoizedTextComponents.tsx @@ -22,7 +22,7 @@ export const MemoizedAnchor = memo( const index = parseInt(match[1], 10) - 1; const associatedDoc = docs?.[index]; if (!associatedDoc) { - return <>{children}; + return {children}; } 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 ( rest.href && window.open(rest.href, "_blank")} + onMouseDown={handleMouseDown} className="cursor-pointer text-link hover:text-link-hover" > {rest.children} diff --git a/web/src/app/chat/message/Messages.tsx b/web/src/app/chat/message/Messages.tsx index 2b08527c5..3e878a2c8 100644 --- a/web/src/app/chat/message/Messages.tsx +++ b/web/src/app/chat/message/Messages.tsx @@ -375,7 +375,11 @@ export const AIMessage = ({ return ( <>
= ({ ...(isDragging ? { zIndex: 1000, position: "relative" as const } : {}), }; + const nameRef = useRef(null); + const hiddenNameRef = useRef(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 (
= ({ : "" } relative flex items-center gap-x-2 py-1 px-2 rounded-md`} > - -

+ + + + +

+ {assistant.name} +

+ + {isNameTruncated && ( + {assistant.name} + )} + + + {assistant.name} -

+
)} -
+
Assistants
@@ -303,6 +349,7 @@ export const HistorySidebar = forwardRef( sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} + modifiers={[restrictToVerticalAxis]} > diff --git a/web/src/app/chat/sessionSidebar/PagesTab.tsx b/web/src/app/chat/sessionSidebar/PagesTab.tsx index 0e1fd5a9b..24d6fc66d 100644 --- a/web/src/app/chat/sessionSidebar/PagesTab.tsx +++ b/web/src/app/chat/sessionSidebar/PagesTab.tsx @@ -13,7 +13,7 @@ import { FiPlus, FiTrash2, FiCheck, FiX } from "react-icons/fi"; import { NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED } from "@/lib/constants"; import { FolderDropdown } from "../folders/FolderDropdown"; 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 { groupSessionsByDateRange } from "../lib"; import React from "react"; @@ -36,6 +36,7 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useChatContext } from "@/components/context/ChatContext"; import { SettingsContext } from "@/components/settings/SettingsProvider"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; interface SortableFolderProps { folder: Folder; @@ -53,34 +54,41 @@ interface SortableFolderProps { const SortableFolder: React.FC = (props) => { const settings = useContext(SettingsContext); const mobile = settings?.isMobile; - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ - id: props.folder.folder_id?.toString() ?? "", - data: { - activationConstraint: { - distance: 8, - }, - }, - disabled: mobile, - }); + const [isDragging, setIsDragging] = useState(false); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging: isDraggingDndKit, + } = useSortable({ + id: props.folder.folder_id?.toString() ?? "", + disabled: mobile, + }); const ref = useRef(null); - const style = { + + const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, + zIndex: isDragging ? 1000 : "auto", + position: isDragging ? "relative" : "static", + opacity: isDragging ? 0.6 : 1, }; + useEffect(() => { + setIsDragging(isDraggingDndKit); + }, [isDraggingDndKit]); + return (
- +
); }; @@ -359,6 +367,7 @@ export function PagesTab({ {folders && folders.length > 0 && ( @@ -76,6 +76,7 @@ export function Modal({ }} className={` bg-background + bg-blue-400 text-emphasis rounded shadow-2xl diff --git a/web/src/components/admin/connectors/Field.tsx b/web/src/components/admin/connectors/Field.tsx index 7b6b8826a..a336e5588 100644 --- a/web/src/components/admin/connectors/Field.tsx +++ b/web/src/components/admin/connectors/Field.tsx @@ -25,11 +25,13 @@ import { } from "@/components/ui/tooltip"; import ReactMarkdown from "react-markdown"; import { FaMarkdown } from "react-icons/fa"; -import { useRef, useState } from "react"; +import { useRef, useState, useCallback } from "react"; import remarkGfm from "remark-gfm"; import { EditIcon } from "@/components/icons/icons"; import { Button } from "@/components/ui/button"; import Link from "next/link"; +import { CheckboxField } from "@/components/ui/checkbox"; +import { CheckedState } from "@radix-ui/react-checkbox"; export function SectionHeader({ children, @@ -51,7 +53,7 @@ export function Label({ return (
{children} @@ -75,7 +77,7 @@ export function LabelWithTooltip({ } export function SubLabel({ children }: { children: string | JSX.Element }) { - return
{children}
; + return
{children}
; } export function ManualErrorMessage({ children }: { children: string }) { @@ -439,53 +441,62 @@ interface BooleanFormFieldProps { name: string; label: string; subtext?: string | JSX.Element; - onChange?: (e: React.ChangeEvent) => void; removeIndent?: boolean; small?: boolean; - alignTop?: boolean; noLabel?: boolean; disabled?: boolean; - checked?: boolean; optional?: boolean; tooltip?: string; + disabledTooltip?: string; } export const BooleanFormField = ({ name, label, subtext, - onChange, removeIndent, noLabel, optional, small, disabled, - alignTop, - checked, tooltip, + disabledTooltip, }: BooleanFormFieldProps) => { - const [field, meta, helpers] = useField(name); - const { setValue } = helpers; + const { setFieldValue } = useFormikContext(); - const handleChange = (e: React.ChangeEvent) => { - setValue(e.target.checked); - if (onChange) { - onChange(e); - } - }; + const handleChange = useCallback( + (checked: CheckedState) => { + if (!disabled) { + setFieldValue(name, checked); + } + }, + [disabled, name, setFieldValue] + ); return (
-