= ({
width,
align = 'left',
}) => {
- const getAlignmentClass = () => {
- switch (align) {
- case 'right':
- return 'text-right';
- case 'center':
- return 'text-center';
- default:
- return 'text-left';
- }
- };
-
- const baseClasses = `px-2 py-2 text-sm lg:px-3 box-border ${getAlignmentClass()}`;
- const widthClasses = minWidth ? minWidth : '';
+ const alignmentClass =
+ align === 'right'
+ ? 'text-right'
+ : align === 'center'
+ ? 'text-center'
+ : 'text-left';
return (
{children}
diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx
new file mode 100644
index 00000000..f72d0b1a
--- /dev/null
+++ b/frontend/src/components/ui/tabs.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+import * as TabsPrimitive from '@radix-ui/react-tabs';
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx
index 26c96fc8..a00da721 100644
--- a/frontend/src/conversation/Conversation.tsx
+++ b/frontend/src/conversation/Conversation.tsx
@@ -364,7 +364,7 @@ export default function Conversation() {
: 'max-w-[1300px] md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12'
}`}
>
-
+
{
diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx
index ad66bbfb..29030434 100644
--- a/frontend/src/conversation/ConversationBubble.tsx
+++ b/frontend/src/conversation/ConversationBubble.tsx
@@ -25,12 +25,19 @@ import Like from '../assets/like.svg?react';
import Link from '../assets/link.svg';
import Sources from '../assets/sources.svg';
import UserIcon from '../assets/user.svg';
-import Accordion from '../components/Accordion';
-import Avatar from '../components/Avatar';
import CopyButton from '../components/CopyButton';
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '../components/ui/accordion';
+import { Avatar } from '../components/ui/avatar';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
import MermaidRenderer from '../components/MermaidRenderer';
-import Sidebar from '../components/Sidebar';
import Spinner from '../components/Spinner';
+import { Sheet, SheetContent } from '../components/ui/sheet';
import SpeakButton from '../components/TextToSpeechButton';
import { useDarkTheme, useOutsideAlerter } from '../hooks';
import {
@@ -152,7 +159,7 @@ const ConversationBubble = forwardRef<
![]()
-
- }
- />
+
+
+
{!isEditClicked && (
<>
-
+
{shouldShowToggle && (
-
+
)}
-
+
>
)}
@@ -238,17 +247,20 @@ const ConversationBubble = forwardRef<
}}
rows={5}
value={editInputBox}
- className="border-border text-carbon dark:border-philippine-grey dark:text-foreground w-full resize-none rounded-3xl border px-4 py-3 text-base leading-relaxed focus:outline-hidden"
+ className="border-border text-foreground dark:border-border dark:text-foreground focus-visible:ring-ring/50 focus-visible:border-ring w-full resize-none rounded-3xl border px-4 py-3 text-base leading-relaxed focus:outline-hidden focus-visible:ring-[3px]"
/>
-
-
)}
@@ -332,14 +344,10 @@ const ConversationBubble = forwardRef<
- }
+ imgClassName="h-full w-full object-fill"
/>
{t('conversation.sources.title')}
@@ -435,7 +443,7 @@ const ConversationBubble = forwardRef<
)}
{!message && primaryArtifactCall?.artifact_id && onOpenArtifact && (
-
onOpenArtifact({
@@ -443,7 +451,7 @@ const ConversationBubble = forwardRef<
toolName: primaryArtifactCall.tool_name,
})
}
- className="flex items-center gap-2 rounded-full bg-purple-100 px-3 py-2 text-sm font-medium text-purple-700 transition-colors hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:hover:bg-purple-900/50"
+ className="h-auto rounded-full bg-purple-100 px-3 py-2 text-sm font-medium text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:hover:bg-purple-900/50"
>
+
)}
{thought && (
@@ -479,21 +487,17 @@ const ConversationBubble = forwardRef<
- }
+ imgClassName="h-full w-full object-cover"
/>
{t('conversation.answer')}
{
const el = document.getElementById(
@@ -541,11 +545,11 @@ const ConversationBubble = forwardRef<
);
}
}}
- className="mx-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-purple-100 px-1.5 text-xs font-semibold text-purple-700 transition-colors hover:bg-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:hover:bg-purple-900/60"
+ className="mx-0.5 h-5 min-w-5 rounded-full bg-purple-100 px-1.5 text-xs font-semibold text-purple-700 hover:bg-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:hover:bg-purple-900/60"
title={`Jump to source ${num}`}
>
{num}
-
+
);
}
return (
@@ -572,8 +576,8 @@ const ConversationBubble = forwardRef<
const language = match ? match[1] : '';
return match ? (
-
-
+
+
{language}
@@ -601,7 +605,7 @@ const ConversationBubble = forwardRef<
) : (
-
+
{children}
);
@@ -690,7 +694,7 @@ const ConversationBubble = forwardRef<
<>
{primaryArtifactCall?.artifact_id && onOpenArtifact && (
-
onOpenArtifact({
@@ -698,7 +702,7 @@ const ConversationBubble = forwardRef<
toolName: primaryArtifactCall.tool_name,
})
}
- className="flex items-center gap-2 rounded-full bg-purple-100 px-3 py-2 text-sm font-medium text-purple-700 transition-colors hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:hover:bg-purple-900/50"
+ className="h-auto rounded-full bg-purple-100 px-3 py-2 text-sm font-medium text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:hover:bg-purple-900/50"
aria-label="View artifacts"
>
+
)}
{!isStreaming && (
@@ -735,8 +739,10 @@ const ConversationBubble = forwardRef<
{research && message && (
- {
const blob = new Blob([message], {
type: 'text/markdown',
@@ -748,7 +754,7 @@ const ConversationBubble = forwardRef<
link.click();
URL.revokeObjectURL(url);
}}
- className="bg-card dark:hover:bg-accent hover:bg-muted flex cursor-pointer items-center justify-center rounded-full p-2 dark:bg-transparent"
+ className="bg-card hover:bg-muted dark:hover:bg-accent h-auto w-auto cursor-pointer rounded-full p-2 dark:bg-transparent"
aria-label="Export as Markdown"
title="Export as Markdown"
>
@@ -764,7 +770,7 @@ const ConversationBubble = forwardRef<
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
-
+
)}
@@ -773,9 +779,11 @@ const ConversationBubble = forwardRef<
{handleFeedback && (
<>
- {
if (feedback === 'LIKE') {
handleFeedback?.(null);
@@ -790,13 +798,15 @@ const ConversationBubble = forwardRef<
-
+
- {
if (feedback === 'DISLIKE') {
handleFeedback?.(null);
@@ -813,7 +823,7 @@ const ConversationBubble = forwardRef<
-
+
>
)}
@@ -824,14 +834,16 @@ const ConversationBubble = forwardRef<
)}
{sources && (
- {
- setIsSidebarOpen(state);
- }}
- >
-
-
+
+
+
+
+
)}
);
@@ -874,9 +886,7 @@ function AllSources(sources: AllSourcesProps) {
{`${index + 1}. ${source.title}`}
@@ -941,10 +951,11 @@ function ToolCallApprovalBar({
- {
@@ -952,12 +963,14 @@ function ToolCallApprovalBar({
}}
>
Approve
-
-
+ {
if (expanded && comment) {
@@ -970,9 +983,12 @@ function ToolCallApprovalBar({
}}
>
Deny
-
-
+ setExpanded(!expanded)}
title="Details"
>
@@ -981,7 +997,7 @@ function ToolCallApprovalBar({
alt="expand"
className={`h-3.5 w-3.5 transition-transform duration-200 dark:invert ${expanded ? 'rotate-180' : ''}`}
/>
-
+
{expanded && (
@@ -992,10 +1008,10 @@ function ToolCallApprovalBar({
{JSON.stringify(toolCall.arguments, null, 2)}
- setComment(e.target.value)}
onKeyDown={(e) => {
@@ -1052,17 +1068,16 @@ function ToolCalls({
<>
- }
+ imgClassName="h-full w-full object-fill"
/>
- setIsToolCallsOpen(!isToolCallsOpen)}
>
Tool Calls
@@ -1071,7 +1086,7 @@ function ToolCalls({
alt="ChevronDown"
className={`h-4 w-4 transform transition-transform duration-200 dark:invert ${isToolCallsOpen ? 'rotate-180' : ''}`}
/>
-
+
{isToolCallsOpen && (
@@ -1091,83 +1106,74 @@ function ToolCalls({
return (
-
-
-
-
- Arguments
- {' '}
-
+
+ {`${toolCall.tool_name} - ${toolCall.action_name.substring(0, toolCall.action_name.lastIndexOf('_'))}`}
+
+
+
+
+
+ Arguments{' '}
+
+
+
+
+ {JSON.stringify(toolCall.arguments, null, 2)}
+
+
+
+
+
+ Response{' '}
+
+
+ {toolCall.status === 'pending' && (
+
+
+
)}
- />
-
-
-
- {JSON.stringify(toolCall.arguments, null, 2)}
-
-
-
-
-
-
- Response
- {' '}
-
-
- {toolCall.status === 'pending' && (
-
-
-
- )}
- {toolCall.status === 'completed' && (
-
-
- {JSON.stringify(toolCall.result, null, 2)}
-
-
- )}
- {toolCall.status === 'error' && (
-
-
- {toolCall.error}
-
-
- )}
- {toolCall.status === 'denied' && (
-
-
- Denied by user
-
-
- )}
-
-
+ {toolCall.status === 'completed' && (
+
+
+ {JSON.stringify(toolCall.result, null, 2)}
+
+
+ )}
+ {toolCall.status === 'error' && (
+
+
+ {toolCall.error}
+
+
+ )}
+ {toolCall.status === 'denied' && (
+
+
+ Denied by user
+
+
+ )}
+
+
+
+
);
})}
@@ -1195,17 +1201,16 @@ function Thought({
- }
+ imgClassName="h-full w-full object-fill"
/>
- setIsThoughtOpen(!isThoughtOpen)}
>
@@ -1216,11 +1221,11 @@ function Thought({
alt="ChevronDown"
className={`h-4 w-4 transform transition-transform duration-200 dark:invert ${isThoughtOpen ? 'rotate-180' : ''}`}
/>
-
+
{isThoughtOpen && (
-
+
-
+
+
{language}
@@ -1256,7 +1261,7 @@ function Thought({
) : (
-
+
{children}
);
diff --git a/frontend/src/conversation/ConversationMessages.tsx b/frontend/src/conversation/ConversationMessages.tsx
index 953d88cb..66eaf064 100644
--- a/frontend/src/conversation/ConversationMessages.tsx
+++ b/frontend/src/conversation/ConversationMessages.tsx
@@ -10,9 +10,9 @@ import { useTranslation } from 'react-i18next';
import ArrowDown from '../assets/arrow-down.svg';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
-import RetryIcon from '../components/RetryIcon';
+import Retry from '../assets/retry.svg?react';
+import { Button } from '../components/ui/button';
import Hero from '../Hero';
-import { useDarkTheme } from '../hooks';
import ConversationBubble from './ConversationBubble';
import { FEEDBACK, Query, Status } from './conversationModels';
@@ -61,7 +61,6 @@ export default function ConversationMessages({
isSplitView = false,
agentId,
}: ConversationMessagesProps) {
- const [isDarkTheme] = useDarkTheme();
const { t } = useTranslation();
const conversationRef = useRef(null);
@@ -237,14 +236,6 @@ export default function ConversationMessages({
};
}, [handleScroll]);
- const retryIconProps = {
- width: 12,
- height: 12,
- fill: isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)',
- stroke: isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)',
- strokeWidth: 10,
- };
-
const renderResponseView = (query: Query, index: number) => {
const isLastMessage = index === queries.length - 1;
const bubbleMargin = isLastMessage
@@ -255,8 +246,10 @@ export default function ConversationMessages({
// tool_calls and would otherwise fall into the answer branch.
if (query.error) {
const retryButton = (
- {
const questionToRetry = queries[index].prompt;
@@ -268,8 +261,12 @@ export default function ConversationMessages({
}}
aria-label={t('Retry') || 'Retry'}
>
-
-
+
+
);
return (
-
+
@@ -357,14 +354,17 @@ export default function ConversationMessages({
className="flex h-full w-full justify-center overflow-y-auto will-change-scroll sm:pt-6 lg:pt-12"
>
{queries.length > 0 && (
- {
userInterruptedRef.current = false;
setInterrupted(false);
scrollConversationToBottom();
}}
aria-label={t('Scroll to bottom') || 'Scroll to bottom'}
- className={`border-border bg-card fixed bottom-40 left-1/2 z-10 flex h-7 w-7 -translate-x-1/2 items-center justify-center rounded-full border transition-all duration-300 ease-in-out md:right-14 md:left-auto md:h-9 md:w-9 md:translate-x-0 ${
+ className={`bg-card fixed bottom-40 left-1/2 z-10 h-7 w-7 -translate-x-1/2 rounded-full transition-all duration-300 ease-in-out md:right-14 md:left-auto md:h-9 md:w-9 md:translate-x-0 ${
scrollButtonVisible
? 'pointer-events-auto scale-100 opacity-100'
: 'pointer-events-none scale-75 opacity-0'
@@ -375,7 +375,7 @@ export default function ConversationMessages({
alt="arrow down"
className="h-4 w-4 opacity-50 filter md:h-5 md:w-5 dark:invert"
/>
-
+
)}
('INACTIVE');
- const menuRef = useRef (null);
const { t } = useTranslation();
useEffect(() => {
setConversationsName(conversation.name);
@@ -75,19 +80,6 @@ export default function ConversationTile({
}
}
- const handleClickOutside = (event: MouseEvent) => {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setOpen(false);
- }
- };
-
- useEffect(() => {
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, []);
-
const preventScroll = useCallback((event: WheelEvent | TouchEvent) => {
event.preventDefault();
}, []);
@@ -134,7 +126,16 @@ export default function ConversationTile({
}
};
- const menuOptions: MenuOption[] = [
+ type ConversationMenuOption = {
+ icon: string;
+ label: string;
+ onClick: (event: SyntheticEvent) => void;
+ variant: 'default' | 'destructive';
+ iconWidth?: number;
+ iconHeight?: number;
+ };
+
+ const menuOptions: ConversationMenuOption[] = [
{
icon: Share,
label: t('convTile.share'),
@@ -143,7 +144,7 @@ export default function ConversationTile({
setShareModalState(true);
setOpen(false);
},
- variant: 'primary',
+ variant: 'default',
iconWidth: 14,
iconHeight: 14,
},
@@ -151,7 +152,7 @@ export default function ConversationTile({
icon: Edit,
label: t('convTile.rename'),
onClick: handleEditConversation,
- variant: 'primary',
+ variant: 'default',
},
{
icon: Trash,
@@ -163,7 +164,7 @@ export default function ConversationTile({
},
iconWidth: 18,
iconHeight: 18,
- variant: 'danger',
+ variant: 'destructive',
},
];
@@ -203,10 +204,10 @@ export default function ConversationTile({
>
{isEdit ? (
- setConversationsName(e.target.value)}
onKeyDown={handleRenameKeyDown}
@@ -218,10 +219,7 @@ export default function ConversationTile({
)}
{(conversationId === conversation.id || isHovered || isOpen) && (
-
+
{isEdit ? (
![]()
-  {
event.stopPropagation();
onClear();
}}
- />
+ >
+
+
) : (
- {
- event.stopPropagation();
- setOpen(!isOpen);
- }}
- className="hover:bg-accent dark:hover:bg-accent mr-2 flex h-6 w-6 items-center justify-center rounded-full transition-colors duration-200"
- >
-
-
+
+
+ {
+ event.stopPropagation();
+ }}
+ className="mr-2 h-6 w-6 rounded-full"
+ >
+
+
+
+
+ {menuOptions.map((option, index) => (
+ {
+ option.onClick(event as unknown as SyntheticEvent);
+ }}
+ >
+
+ {option.label}
+
+ ))}
+
+
)}
-
)}
diff --git a/frontend/src/conversation/ResearchProgress.tsx b/frontend/src/conversation/ResearchProgress.tsx
index a73594bd..1d86832d 100644
--- a/frontend/src/conversation/ResearchProgress.tsx
+++ b/frontend/src/conversation/ResearchProgress.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import ResearchIcon from '../assets/research.svg';
-import Avatar from '../components/Avatar';
+import { Avatar } from '../components/ui/avatar';
+import { Button } from '../components/ui/button';
import { ResearchState } from './conversationModels';
const SmallCheck = () => (
@@ -100,17 +101,16 @@ export default function ResearchProgress({
{/* Header row — matches Reasoning / Sources pattern */}
- }
+ imgClassName="h-full w-full object-fill"
/>
- setIsExpanded(!isExpanded)}
>
@@ -129,7 +129,7 @@ export default function ResearchProgress({
d="M19 9l-7 7-7-7"
/>
-
+
{status && }
diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx
index 556f18f7..133c51ea 100644
--- a/frontend/src/conversation/SharedConversation.tsx
+++ b/frontend/src/conversation/SharedConversation.tsx
@@ -5,6 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import conversationService from '../api/services/conversationService';
import MessageInput from '../components/MessageInput';
+import { Button } from '../components/ui/button';
import { selectToken } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import { formatDate } from '../utils/dateTimeUtils';
@@ -139,17 +140,17 @@ export const SharedConversation = () => {
twitterDescription="Shared conversations with DocsGPT"
/>
-
-
+
+
{title}
-
+
{t('sharedConv.subtitle')}{' '}
DocsGPT
-
+
{date}
@@ -172,12 +173,13 @@ export const SharedConversation = () => {
/>
) : (
- navigate('/')}
- className="bg-primary hover:bg-primary/90 mb-14 w-fit rounded-full px-5 py-3 text-white shadow-xl transition-colors duration-200 sm:mb-0"
+ className="mb-14 h-auto w-fit rounded-full px-5 py-3 text-white shadow-xl sm:mb-0"
>
{t('sharedConv.button')}
-
+
)}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index a2b64818..6228d11f 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -9,68 +9,11 @@ layer(base);
@theme {
--font-roboto: Roboto, sans-serif;
-
- --color-eerie-black: #212121;
- --color-black-1000: #343541;
- --color-jet: #343541;
- --color-gray-alpha: rgba(0, 0, 0, 0.64);
- --color-gray-1000: #f6f6f6;
- --color-gray-2000: rgba(0, 0, 0, 0.5);
- --color-gray-3000: rgba(243, 243, 243, 1);
- --color-gray-5000: #bbbbbb;
- --color-gray-6000: #757575;
- --color-red-1000: rgb(254, 202, 202);
- --color-blue-1000: #7d54d1;
- --color-blue-2000: #002b49;
- --color-blue-3000: #4b02e2;
- --color-purple-30: #7d54d1;
- --color-purple-3000: rgb(230, 222, 247);
- --color-blue-4000: rgba(0, 125, 255, 0.36);
- --color-blue-5000: rgba(0, 125, 255);
- --color-green-2000: #0fff50;
- --color-light-gray: #edeef0;
- --color-just-black: #00000;
- --color-purple-taupe: #464152;
- --color-dove-gray: #6c6c6c;
- --color-silver: #c4c4c4;
- --color-rainy-gray: #a4a4a4;
- --color-raisin-black: #222327;
- --color-chinese-black: #161616;
- --color-chinese-silver: #cdcdcd;
- --color-dark-charcoal: #2f3036;
- --color-bright-gray: #ebebeb;
- --color-outer-space: #444654;
- --color-gun-metal: #2e303e;
- --color-sonic-silver: #747474;
- --color-soap: #d8ccf1;
- --color-independence: #54546d;
- --color-philippine-yellow: #ffc700;
- --color-chinese-white: #e0e0e0;
- --color-dark-gray: #aaaaaa;
- --color-dim-gray: #6a6a6a;
- --color-cultured: #f4f4f4;
- --color-charleston-green: #2b2c31;
- --color-charleston-green-2: #26272e;
- --color-charleston-green-3: #26272a;
- --color-grey: #7e7e7e;
- --color-platinum: #e6e6e6;
- --color-eerie-black-2: #191919;
- --color-light-silver: #d9d9d9;
- --color-carbon: #2e2e2e;
- --color-onyx: #35363b;
- --color-royal-purple: #6c4ab0;
- --color-chinese-black-2: #0f1419;
- --color-gainsboro: #d9dcde;
- --color-onyx-2: #35383c;
- --color-philippine-grey: #929292;
- --color-charcoal-grey: #53545d;
- --color-north-texas-green: #0c9d35;
- --color-medium-purple: #8d66dd;
- --color-slate-blue: #6f5fca;
- --color-light-gainsboro: #d7d7d7;
- --color-raisin-black-light: #18181b;
- --color-sonic-silver-light: #7f7f82;
- --color-violets-are-blue: #976af3;
+ --font-mono:
+ 'IBMPlexMono-Medium', ui-monospace, SFMono-Regular, Menlo, Consolas,
+ monospace;
+ --shadow-modal: 0px 4px 40px -3px #0000001a;
+ --shadow-toast: 0px 24px 48px 0px #00000029;
}
/*
@@ -745,7 +688,7 @@ Avoid over-scrolling in mobile browsers
}
.logs-table {
- font-family: 'IBMPlexMono-Medium', system-ui;
+ font-family: var(--font-mono);
}
.fade-in {
@@ -814,6 +757,12 @@ Avoid over-scrolling in mobile browsers
transform: translateY(0);
}
}
+
+ @keyframes spinner-rotate {
+ 100% {
+ transform: rotate(1turn);
+ }
+ }
}
/*
diff --git a/frontend/src/modals/AddActionModal.tsx b/frontend/src/modals/AddActionModal.tsx
index b6cfbeb9..4f19191b 100644
--- a/frontend/src/modals/AddActionModal.tsx
+++ b/frontend/src/modals/AddActionModal.tsx
@@ -1,9 +1,10 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import Input from '../components/Input';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
-import WrapperModal from './WrapperModal';
type AddActionModalProps = {
modalState: ActiveState;
@@ -36,56 +37,62 @@ export default function AddActionModal({
setModalState('INACTIVE');
};
- if (modalState !== 'ACTIVE') return null;
+ const handleCancel = () => {
+ setFunctionNameError(false);
+ setModalState('INACTIVE');
+ setActionName('');
+ };
+
return (
- setModalState('INACTIVE')} className="sm:w-lg">
-
-
- {t('modals.addAction.title')}
-
-
- {
- const value = e.target.value;
- setActionName(value);
- setFunctionNameError(!isValidFunctionName(value));
- }}
- borderVariant="thin"
- labelBgClassName="bg-card"
- placeholder={t('modals.addAction.actionNamePlaceholder')}
- required={true}
- />
-
- {functionNameError
- ? t('modals.addAction.invalidFormat')
- : t('modals.addAction.formatHelp')}
-
-
-
-
- {t('modals.addAction.addButton')}
-
- {
- setFunctionNameError(false);
- setModalState('INACTIVE');
- setActionName('');
- }}
- className="dark:text-foreground hover:bg-accent dark:hover:bg-accent cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium"
+ {
+ if (!open) handleCancel();
+ }}
+ title={t('modals.addAction.title')}
+ footer={
+ <>
+
{t('modals.configTool.closeButton')}
-
-
+
+
+ {t('modals.addAction.addButton')}
+
+ >
+ }
+ >
+
+ {
+ const value = e.target.value;
+ setActionName(value);
+ setFunctionNameError(!isValidFunctionName(value));
+ }}
+ labelBgClassName="bg-card"
+ label={t('modals.addAction.actionNamePlaceholder')}
+ required={true}
+ />
+
+ {functionNameError
+ ? t('modals.addAction.invalidFormat')
+ : t('modals.addAction.formatHelp')}
+
-
+
);
}
diff --git a/frontend/src/modals/AddToolModal.tsx b/frontend/src/modals/AddToolModal.tsx
index 6389b4d2..fa340694 100644
--- a/frontend/src/modals/AddToolModal.tsx
+++ b/frontend/src/modals/AddToolModal.tsx
@@ -1,16 +1,16 @@
-import React, { useRef } from 'react';
+import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import SkeletonLoader from '../components/SkeletonLoader';
-import { useLoaderState, useOutsideAlerter } from '../hooks';
+import { Modal } from '../components/ui/modal';
+import { useLoaderState } from '../hooks';
import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import ConfigToolModal from './ConfigToolModal';
import MCPServerModal from './MCPServerModal';
import { AvailableToolType } from './types';
-import WrapperComponent from './WrapperModal';
export default function AddToolModal({
message,
@@ -27,7 +27,6 @@ export default function AddToolModal({
}) {
const { t } = useTranslation();
const token = useSelector(selectToken);
- const modalRef = useRef (null);
const [availableTools, setAvailableTools] = React.useState<
AvailableToolType[]
>([]);
@@ -39,12 +38,6 @@ export default function AddToolModal({
React.useState('INACTIVE');
const [loading, setLoading] = useLoaderState(false);
- useOutsideAlerter(modalRef, () => {
- if (modalState === 'ACTIVE') {
- setModalState('INACTIVE');
- }
- }, [modalState]);
-
const getAvailableTools = () => {
setLoading(true);
userService
@@ -109,69 +102,67 @@ export default function AddToolModal({
return (
<>
- {modalState === 'ACTIVE' && (
- setModalState('INACTIVE')}
- className="h-[85vh] w-[90vw] max-w-[950px] md:w-[85vw] lg:w-[75vw]"
- >
-
-
-
- {t('settings.tools.selectToolSetup')}
-
-
- {loading ? (
-
-
-
- ) : (
-
- {availableTools.map((tool, index) => (
- {
+ !o && setModalState('INACTIVE')}
+ title={t('settings.tools.selectToolSetup')}
+ size="xl"
+ className="h-[85vh] w-[90vw] sm:max-w-[950px] md:w-[85vw] lg:w-[75vw]"
+ >
+
+
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {availableTools.map((tool, index) => (
+ {
+ setSelectedTool(tool);
+ handleAddTool(tool);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
setSelectedTool(tool);
handleAddTool(tool);
- }}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- setSelectedTool(tool);
- handleAddTool(tool);
- }
- }}
- >
-
-
- 
-
-
-
- {tool.displayName}
-
-
- {tool.description}
-
-
+ }
+ }}
+ >
+
+
+ 
+
+
+
+ {tool.displayName}
+
+
+ {tool.description}
+
- ))}
-
- )}
-
+
+ ))}
+
+ )}
-
- )}
+
+
{
- setModalState('INACTIVE');
- }}
+ !o && setModalState('INACTIVE')}
+ title={t('modals.agentDetails.title')}
+ size="md"
>
-
- {t('modals.agentDetails.title')}
-
@@ -100,7 +97,7 @@ export default function AgentDetailsModal({
{sharedToken ? (
) : (
-
{loadingStates.publicLink ? (
-
+
) : (
t('modals.agentDetails.generate')
)}
-
+
)}
@@ -150,7 +149,7 @@ export default function AgentDetailsModal({
{apiKey ? (
-
+
{apiKey}
{!apiKey.includes('...') && (
@@ -178,9 +177,13 @@ export default function AgentDetailsModal({
) : (
-
+
{t('modals.agentDetails.generate')}
-
+
)}
@@ -191,7 +194,7 @@ export default function AgentDetailsModal({
{webhookUrl ? (
-
+
{webhookUrl}
@@ -218,20 +221,22 @@ export default function AgentDetailsModal({
) : (
-
{loadingStates.webhook ? (
-
+
) : (
t('modals.agentDetails.generate')
)}
-
+
)}
-
+
);
}
diff --git a/frontend/src/modals/ConfigToolModal.tsx b/frontend/src/modals/ConfigToolModal.tsx
index 4097047c..7f657a07 100644
--- a/frontend/src/modals/ConfigToolModal.tsx
+++ b/frontend/src/modals/ConfigToolModal.tsx
@@ -4,12 +4,13 @@ import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import ConfigFields from '../components/ConfigFields';
+import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import { AvailableToolType } from './types';
-import WrapperModal from './WrapperModal';
interface ConfigToolModalProps {
modalState: ActiveState;
@@ -110,14 +111,38 @@ export default function ConfigToolModal({
.finally(() => setSaving(false));
};
- if (modalState !== 'ACTIVE' || !tool) return null;
+ if (!tool) return null;
return (
-
-
-
- {t('modals.configTool.title')}
-
+ !o && handleClose()}
+ title={t('modals.configTool.title')}
+ size="lg"
+ footer={
+ <>
+
+ {t('modals.configTool.closeButton')}
+
+
+ {saving
+ ? t('modals.configTool.addButton') + '…'
+ : t('modals.configTool.addButton')}
+
+ >
+ }
+ >
+
{t('modals.configTool.type')}:{' '}
@@ -149,25 +174,7 @@ export default function ConfigToolModal({
/>
)}
-
-
-
- {saving
- ? t('modals.configTool.addButton') + '…'
- : t('modals.configTool.addButton')}
-
-
- {t('modals.configTool.closeButton')}
-
-
-
+
);
}
diff --git a/frontend/src/modals/ConfirmationModal.tsx b/frontend/src/modals/ConfirmationModal.tsx
index 9883b446..9c035166 100644
--- a/frontend/src/modals/ConfirmationModal.tsx
+++ b/frontend/src/modals/ConfirmationModal.tsx
@@ -1,7 +1,8 @@
import { useTranslation } from 'react-i18next';
+import { Button } from '../components/ui/button';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
-import WrapperModal from './WrapperModal';
export default function ConfirmationModal({
message,
@@ -24,11 +25,6 @@ export default function ConfirmationModal({
}) {
const { t } = useTranslation();
- const submitButtonClasses =
- variant === 'danger'
- ? 'rounded-3xl bg-destructive px-5 py-2 text-sm text-white transition-all hover:bg-destructive/90 hover:font-bold tracking-[0.019em] hover:tracking-normal'
- : 'rounded-3xl bg-primary px-5 py-2 text-sm text-white transition-all hover:bg-primary/90';
-
const handleSubmitClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -44,34 +40,34 @@ export default function ConfirmationModal({
};
return (
- <>
- {modalState === 'ACTIVE' && (
- setModalState('INACTIVE')}>
-
-
-
- {message}
-
-
-
-
- {submitLabel}
-
-
- {cancelLabel ? cancelLabel : t('cancel')}
-
-
-
-
-
-
- )}
- >
+ {
+ if (!open) setModalState('INACTIVE');
+ }}
+ title={message}
+ footer={
+ <>
+
+ {cancelLabel ? cancelLabel : t('cancel')}
+
+
+ {submitLabel}
+
+ >
+ }
+ >
+ {null}
+
);
}
diff --git a/frontend/src/modals/CustomModelModal.tsx b/frontend/src/modals/CustomModelModal.tsx
index 61e7b2e6..311b022f 100644
--- a/frontend/src/modals/CustomModelModal.tsx
+++ b/frontend/src/modals/CustomModelModal.tsx
@@ -5,11 +5,12 @@ import { useSelector } from 'react-redux';
import customModelsService from '../api/services/customModelsService';
import Spinner from '../components/Spinner';
+import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
-import WrapperComponent from './WrapperModal';
+import { Modal } from '../components/ui/modal';
import type { CreateCustomModelPayload, CustomModel } from '../models/types';
@@ -285,12 +286,17 @@ export default function CustomModelModal({
}
};
- if (modalState !== 'ACTIVE') return null;
-
return (
- !o && closeModal()}
+ hideTitle
+ title={
+ isEditMode
+ ? t('settings.customModels.editTitle')
+ : t('settings.customModels.addTitle')
+ }
+ size="lg"
className="max-w-[600px] md:w-[80vw] lg:w-[60vw]"
>
@@ -365,6 +371,10 @@ export default function CustomModelModal({
handleChange('base_url', e.target.value)}
placeholder={t('settings.customModels.placeholders.baseUrl')}
@@ -390,8 +400,11 @@ export default function CustomModelModal({
handleChange('api_key', e.target.value)}
placeholder={
@@ -519,12 +532,13 @@ export default function CustomModelModal({
-
{testing ? (
@@ -536,21 +550,22 @@ export default function CustomModelModal({
) : (
t('settings.customModels.testConnection')
)}
-
+
-
{t('cancel')}
-
-
+
{saving ? (
@@ -562,12 +577,12 @@ export default function CustomModelModal({
) : (
t('settings.customModels.save')
)}
-
+
-
+
);
}
@@ -579,19 +594,20 @@ interface CapabilityChipProps {
function CapabilityChip({ label, active, onClick }: CapabilityChipProps) {
return (
-
{active && }
{label}
-
+
);
}
diff --git a/frontend/src/modals/DeleteConvModal.tsx b/frontend/src/modals/DeleteConvModal.tsx
deleted file mode 100644
index 5d08301f..00000000
--- a/frontend/src/modals/DeleteConvModal.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react';
-import { useDispatch } from 'react-redux';
-import { ActiveState } from '../models/misc';
-import { useMediaQuery, useOutsideAlerter } from '../hooks';
-import ConfirmationModal from './ConfirmationModal';
-import { useTranslation } from 'react-i18next';
-import { Action } from '@reduxjs/toolkit';
-
-export default function DeleteConvModal({
- modalState,
- setModalState,
- handleDeleteAllConv,
-}: {
- modalState: ActiveState;
- setModalState: (val: ActiveState) => Action;
- handleDeleteAllConv: () => void;
-}) {
- const modalRef = React.useRef(null);
- const dispatch = useDispatch();
- const { isMobile } = useMediaQuery();
- const { t } = useTranslation();
- useOutsideAlerter(modalRef, () => {
- if (isMobile && modalState === 'ACTIVE') {
- dispatch(setModalState('INACTIVE'));
- }
- }, [modalState]);
-
- function handleSubmit() {
- handleDeleteAllConv();
- dispatch(setModalState('INACTIVE'));
- }
-
- function handleCancel() {
- dispatch(setModalState('INACTIVE'));
- }
-
- return (
- dispatch(setModalState(state))}
- submitLabel={t('modals.deleteConv.delete')}
- handleSubmit={handleSubmit}
- handleCancel={handleCancel}
- variant="danger"
- />
- );
-}
diff --git a/frontend/src/modals/FolderManagementModal.tsx b/frontend/src/modals/FolderManagementModal.tsx
index a942ccd1..e09cca65 100644
--- a/frontend/src/modals/FolderManagementModal.tsx
+++ b/frontend/src/modals/FolderManagementModal.tsx
@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
-import WrapperModal from './WrapperModal';
type FolderNameModalProps = {
modalState: ActiveState;
@@ -42,46 +44,54 @@ export default function FolderNameModal({
}
};
- if (modalState !== 'ACTIVE') return null;
+ const handleCancel = () => {
+ setModalState('INACTIVE');
+ setName('');
+ };
return (
- setModalState('INACTIVE')}>
-
-
- {mode === 'create'
- ? t('agents.folders.newFolder')
- : t('agents.folders.rename')}
-
- setName(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder={t('agents.folders.folderName')}
- autoFocus
- className="border-border bg-card w-full rounded-lg border px-3 py-2 text-sm outline-none dark:text-white"
- />
-
- {
- setModalState('INACTIVE');
- setName('');
- }}
- className="dark:text-foreground hover:bg-accent dark:hover:bg-accent cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium"
+ {
+ if (!open) handleCancel();
+ }}
+ size="sm"
+ title={
+ mode === 'create'
+ ? t('agents.folders.newFolder')
+ : t('agents.folders.rename')
+ }
+ footer={
+ <>
+
{t('cancel')}
-
-
+
{mode === 'create'
? t('agents.folders.createFolder')
: t('agents.folders.rename')}
-
-
-
-
+
+ >
+ }
+ >
+ setName(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={t('agents.folders.folderName')}
+ autoFocus
+ />
+
);
}
diff --git a/frontend/src/modals/ImportSpecModal.tsx b/frontend/src/modals/ImportSpecModal.tsx
index 645ed283..69cf382d 100644
--- a/frontend/src/modals/ImportSpecModal.tsx
+++ b/frontend/src/modals/ImportSpecModal.tsx
@@ -5,10 +5,13 @@ import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Upload from '../assets/upload.svg';
import Spinner from '../components/Spinner';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import { APIActionType } from '../settings/types';
-import WrapperModal from './WrapperModal';
+import { getMethodColorClass } from '../utils/httpMethodColors';
interface ImportSpecModalProps {
modalState: ActiveState;
@@ -26,18 +29,6 @@ interface ParsedResult {
actions: APIActionType[];
}
-const METHOD_COLORS: Record = {
- GET: 'bg-[#D1FAE5] text-[#065F46] dark:bg-[#064E3B]/60 dark:text-[#6EE7B7]',
- POST: 'bg-[#DBEAFE] text-[#1E40AF] dark:bg-[#1E3A8A]/60 dark:text-[#93C5FD]',
- PUT: 'bg-[#FEF3C7] text-[#92400E] dark:bg-[#78350F]/60 dark:text-[#FCD34D]',
- DELETE:
- 'bg-[#FEE2E2] text-[#991B1B] dark:bg-[#7F1D1D]/60 dark:text-[#FCA5A5]',
- PATCH: 'bg-[#EDE9FE] text-[#5B21B6] dark:bg-[#4C1D95]/60 dark:text-[#C4B5FD]',
- HEAD: 'bg-[#F3F4F6] text-[#374151] dark:bg-[#374151]/60 dark:text-[#D1D5DB]',
- OPTIONS:
- 'bg-[#F3F4F6] text-[#374151] dark:bg-[#374151]/60 dark:text-[#D1D5DB]',
-};
-
export default function ImportSpecModal({
modalState,
setModalState,
@@ -157,19 +148,47 @@ export default function ImportSpecModal({
handleClose();
};
- if (modalState !== 'ACTIVE') return null;
-
return (
- !o && handleClose()}
+ title={t('modals.importSpec.title')}
+ size="lg"
contentClassName="max-h-[70vh]"
+ footer={
+ <>
+
+ {t('modals.importSpec.cancel')}
+
+ {!parsedResult ? (
+
+ {loading && }
+ {!loading && t('modals.importSpec.parse')}
+
+ ) : (
+
+ {t('modals.importSpec.import', { count: selectedActions.size })}
+
+ )}
+ >
+ }
>
-
- {t('modals.importSpec.title')}
-
-
{!parsedResult ? (
@@ -223,11 +242,11 @@ export default function ImportSpecModal({
- setBaseUrl(e.target.value)}
- className="border-border dark:border-border text-foreground dark:text-foreground bg-card w-full rounded-lg border px-3 py-2 text-sm outline-hidden"
+ className="bg-card h-auto rounded-lg px-3 py-2 text-sm md:text-sm"
placeholder={
parsedResult.metadata.base_url || 'https://api.example.com'
}
@@ -241,14 +260,17 @@ export default function ImportSpecModal({
count: parsedResult.actions.length,
})}
-
{selectedActions.size === parsedResult.actions.length
? t('modals.importSpec.deselectAll')
: t('modals.importSpec.selectAll')}
-
+
@@ -266,7 +288,7 @@ export default function ImportSpecModal({
{action.method.toUpperCase()}
@@ -288,34 +310,7 @@ export default function ImportSpecModal({
)}
-
-
- {!parsedResult ? (
-
- {loading && }
- {!loading && t('modals.importSpec.parse')}
-
- ) : (
-
- {t('modals.importSpec.import', { count: selectedActions.size })}
-
- )}
-
- {t('modals.importSpec.cancel')}
-
-
-
+
);
}
diff --git a/frontend/src/modals/JWTModal.tsx b/frontend/src/modals/JWTModal.tsx
index 800181fd..9734089d 100644
--- a/frontend/src/modals/JWTModal.tsx
+++ b/frontend/src/modals/JWTModal.tsx
@@ -1,8 +1,9 @@
import React, { useState } from 'react';
-import Input from '../components/Input';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
-import WrapperModal from './WrapperModal';
type JWTModalProps = {
modalState: ActiveState;
@@ -15,20 +16,16 @@ export default function JWTModal({
}: JWTModalProps) {
const [jwtToken, setJwtToken] = useState ('');
- if (modalState !== 'ACTIVE') return null;
-
return (
- {
+ /* uncloseable by design; P1.7 revisits */
+ }}
isPerformingTask={true}
- close={() => undefined}
+ title="Add JWT Token"
>
-
-
- Add JWT Token
-
-
setJwtToken(e.target.value)}
- borderVariant="thin"
data-testid="jwt-token-input"
/>
-
Save Token
-
+
-
+
);
}
diff --git a/frontend/src/modals/MCPServerModal.tsx b/frontend/src/modals/MCPServerModal.tsx
index 44b47625..c568b0fb 100644
--- a/frontend/src/modals/MCPServerModal.tsx
+++ b/frontend/src/modals/MCPServerModal.tsx
@@ -5,6 +5,7 @@ import { useSelector } from 'react-redux';
import { baseURL } from '../api/client';
import userService from '../api/services/userService';
import Spinner from '../components/Spinner';
+import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
import {
@@ -17,7 +18,7 @@ import {
import { ActiveState } from '../models/misc';
import { selectRecentEvents } from '../notifications/notificationsSlice';
import { selectToken } from '../preferences/preferenceSlice';
-import WrapperComponent from './WrapperModal';
+import { Modal } from '../components/ui/modal';
interface MCPServerModalProps {
modalState: ActiveState;
@@ -576,255 +577,256 @@ export default function MCPServerModal({
};
return (
- modalState === 'ACTIVE' && (
- {
+ {
+ if (!o) {
setModalState('INACTIVE');
resetForm();
- }}
- className="max-w-[600px] md:w-[80vw] lg:w-[60vw]"
- >
-
-
-
- {server
- ? t('settings.tools.mcp.reconnectServer', {
- defaultValue: 'Reconnect Server',
- })
- : t('settings.tools.mcp.addServer')}
-
-
-
-
- {server?.has_encrypted_credentials &&
- formData.auth_type !== 'oauth' && (
-
- {t('settings.tools.mcp.reenterCredentials', {
- defaultValue:
- 'Re-enter your credentials to test and update the connection.',
- })}
-
- )}
-
-
- handleInputChange('name', e.target.value)}
- placeholder={t('settings.tools.mcp.serverName')}
- aria-invalid={!!errors.name || undefined}
- className="rounded-xl"
- />
- {errors.name && (
- {errors.name}
- )}
-
-
-
-
-
- handleInputChange('server_url', e.target.value)
- }
- placeholder="https://example.com/mcp"
- aria-invalid={!!errors.server_url || undefined}
- className="rounded-xl"
- />
- {errors.server_url && (
-
- {errors.server_url}
-
- )}
-
-
-
-
-
-
-
- {renderAuthFields()}
-
-
-
- {
- const value = e.target.value;
- if (value === '') {
- handleInputChange('timeout', '');
- } else {
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue >= 1) {
- handleInputChange('timeout', numValue);
- }
- }
- }}
- placeholder="30"
- min={1}
- max={300}
- aria-invalid={!!errors.timeout || undefined}
- className="rounded-xl"
- />
- {errors.timeout && (
- {errors.timeout}
- )}
-
-
- {testResult && (
-
- {testResult.message}
- {testResult.authorization_url && (
- {
- e.preventDefault();
- const popup = window.open(
- testResult.authorization_url,
- 'oauthPopup',
- 'width=600,height=700',
- );
- if (popup) oauthPopupRef.current = popup;
- }}
- className="mt-1.5 inline-block font-medium underline"
- >
- {t('settings.tools.mcp.openAuthPage', {
- defaultValue: 'Open authorization page',
- })}
-
- )}
+ }
+ }}
+ title={
+ server
+ ? t('settings.tools.mcp.reconnectServer', {
+ defaultValue: 'Reconnect Server',
+ })
+ : t('settings.tools.mcp.addServer')
+ }
+ size="lg"
+ className="max-w-[600px] md:w-[80vw] lg:w-[60vw]"
+ >
+
+
+
+ {server?.has_encrypted_credentials &&
+ formData.auth_type !== 'oauth' && (
+
+ {t('settings.tools.mcp.reenterCredentials', {
+ defaultValue:
+ 'Re-enter your credentials to test and update the connection.',
+ })}
)}
-
- {discoveredTools.length > 0 && testResult?.success && (
-
-
- {t('settings.tools.mcp.discoveredTools', {
- count: discoveredTools.length,
- defaultValue: `Discovered Actions (${discoveredTools.length})`,
- })}
-
-
- {discoveredTools.map((tool) => (
- -
- ●
-
-
- {tool.name}
-
- {tool.description && (
-
- {tool.description}
-
- )}
-
-
- ))}
-
-
- )}
- {errors.general && (
-
- {errors.general}
-
+
+
+ handleInputChange('name', e.target.value)}
+ placeholder={t('settings.tools.mcp.serverName')}
+ aria-invalid={!!errors.name || undefined}
+ className="rounded-xl"
+ />
+ {errors.name && (
+ {errors.name}
)}
-
-
-
+
+
+
+
+
+
+ {renderAuthFields()}
+
+
+
+ {
+ const value = e.target.value;
+ if (value === '') {
+ handleInputChange('timeout', '');
+ } else {
+ const numValue = parseInt(value);
+ if (!isNaN(numValue) && numValue >= 1) {
+ handleInputChange('timeout', numValue);
+ }
+ }
+ }}
+ placeholder="30"
+ min={1}
+ max={300}
+ aria-invalid={!!errors.timeout || undefined}
+ className="rounded-xl"
+ />
+ {errors.timeout && (
+ {errors.timeout}
+ )}
+
+
+ {testResult && (
+
+ )}
+
+ {discoveredTools.length > 0 && testResult?.success && (
+
+
+ {t('settings.tools.mcp.discoveredTools', {
+ count: discoveredTools.length,
+ defaultValue: `Discovered Actions (${discoveredTools.length})`,
+ })}
+
+
+ {discoveredTools.map((tool) => (
+ -
+ ●
+
+
+ {tool.name}
+
+ {tool.description && (
+
+ {tool.description}
+
+ )}
+
+
+ ))}
+
+
+ )}
+ {errors.general && (
+
+ {errors.general}
+
+ )}
+
+
+
+
+
+
+ {testing ? (
+
+
+
+ {t('settings.tools.mcp.testing')}
+
+
+ ) : (
+ t('settings.tools.mcp.testConnection')
+ )}
+
+
+
+ {
+ setModalState('INACTIVE');
+ resetForm();
+ }}
+ className="w-full rounded-3xl px-6 sm:w-auto"
+ >
+ {t('settings.tools.mcp.cancel')}
+
+
+ {loading ? (
- {t('settings.tools.mcp.testing')}
+ {t('settings.tools.mcp.saving')}
) : (
- t('settings.tools.mcp.testConnection')
+ t('settings.tools.mcp.save')
)}
-
-
-
- {
- setModalState('INACTIVE');
- resetForm();
- }}
- className="dark:text-foreground hover:bg-accent dark:hover:bg-muted/50 w-full cursor-pointer rounded-3xl px-6 py-2 text-sm font-medium sm:w-auto"
- >
- {t('settings.tools.mcp.cancel')}
-
-
- {loading ? (
-
-
-
- {t('settings.tools.mcp.saving')}
-
-
- ) : (
- t('settings.tools.mcp.save')
- )}
-
-
+
-
- )
+
+
);
}
diff --git a/frontend/src/modals/MoveToFolderModal.tsx b/frontend/src/modals/MoveToFolderModal.tsx
index bbe4f1e1..8e2420d0 100644
--- a/frontend/src/modals/MoveToFolderModal.tsx
+++ b/frontend/src/modals/MoveToFolderModal.tsx
@@ -6,9 +6,11 @@ import { AgentFolder } from '../agents/types';
import userService from '../api/services/userService';
import ChevronRight from '../assets/chevron-right.svg';
import FolderIcon from '../assets/folder.svg';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Modal } from '../components/ui/modal';
import { ActiveState } from '../models/misc';
import { selectToken, setAgentFolders } from '../preferences/preferenceSlice';
-import WrapperModal from './WrapperModal';
type MoveToFolderModalProps = {
modalState: ActiveState;
@@ -132,34 +134,31 @@ export default function MoveToFolderModal({
}
};
- if (modalState !== 'ACTIVE') return null;
-
return (
- setModalState('INACTIVE')} className="p-0!">
-
+ !o && setModalState('INACTIVE')}
+ size="md"
+ className="w-[800px] max-w-[90vw] p-0! sm:max-w-[90vw]"
+ contentClassName="overflow-visible"
+ hideTitle
+ title={`${t('agents.folders.move')} "${agentName}" to`}
+ >
+
-
+
{t('agents.folders.move')} "{agentName}" to
-
-
+ handleNavigateToPath(-1)}
- className={`hover:text-foreground dark:hover:text-white ${folderPath.length > 0 ? 'opacity-70' : ''}`}
+ className={`hover:text-foreground h-auto px-0 py-0 text-xs font-semibold hover:bg-transparent dark:hover:text-white ${folderPath.length > 0 ? 'opacity-70' : ''}`}
>
{t('agents.filters.byMe')}
-
+
{breadcrumbItems.map((item, index) => (
) : (
- handleNavigateToPath(index)}
- className="hover:text-foreground opacity-70 dark:hover:text-white"
+ className="hover:text-foreground h-auto px-0 py-0 text-xs font-semibold opacity-70 hover:bg-transparent dark:hover:text-white"
>
{item.name}
-
+
)}
))}
@@ -191,7 +192,7 @@ export default function MoveToFolderModal({
{isLoading ? (
-
+
{t('loading')}...
@@ -199,14 +200,16 @@ export default function MoveToFolderModal({
{/* Option to move to root (no folder) - only show at root level */}
{currentFolderId && folderPath.length === 0 && (
- {
e.stopPropagation();
setSelectedFolderId(null);
}}
- className={`dark:border-border flex w-full items-center gap-2 border-b border-gray-200 px-8 py-2 text-left text-[14px] ${
+ className={`dark:border-border h-auto w-full justify-start gap-2 rounded-none border-b border-gray-200 px-8 py-2 text-left text-sm ${
selectedFolderId === null
- ? 'bg-[#7D54D1] text-white'
+ ? 'bg-primary hover:bg-primary text-white hover:text-white'
: 'bg-muted hover:bg-accent dark:bg-muted'
}`}
>
@@ -219,16 +222,18 @@ export default function MoveToFolderModal({
>
{t('agents.folders.noFolder')}
-
+
)}
{currentLevelFolders.map((folder) => (
- setSelectedFolderId(folder.id)}
- className={`dark:border-border flex w-full cursor-pointer items-center justify-between border-b border-gray-200 px-8 py-2 text-left text-[14px] ${
+ className={`dark:border-border h-auto w-full justify-between rounded-none border-b border-gray-200 px-8 py-2 text-left text-sm ${
selectedFolderId === folder.id
- ? 'bg-[#7D54D1] text-white'
+ ? 'bg-primary hover:bg-primary text-white hover:text-white'
: 'bg-muted hover:bg-accent dark:bg-muted'
}`}
>
@@ -259,7 +264,7 @@ export default function MoveToFolderModal({
handleNavigateIntoFolder(folder.id);
}
}}
- className="ml-2 flex h-6 w-6 items-center justify-center rounded-full hover:bg-[#FFFFFF2B]"
+ className="hover:bg-accent ml-2 flex h-6 w-6 items-center justify-center rounded-full"
>
)}
-
+
))}
{currentLevelFolders.length === 0 && folderPath.length > 0 && (
@@ -288,7 +293,7 @@ export default function MoveToFolderModal({
{isCreatingFolder ? (
-
) : (
- {
e.stopPropagation();
setIsCreatingFolder(true);
setTimeout(() => newFolderInputRef.current?.focus(), 0);
}}
- className="rounded-full border border-[#7D54D1] bg-transparent px-6 py-2 text-sm font-medium text-[#7D54D1] hover:bg-[#E5DDF6]"
+ className="border-primary text-primary hover:bg-primary/10 hover:text-primary h-auto rounded-full bg-transparent px-6 py-2 text-sm font-medium"
>
{t('agents.folders.newFolder')}
-
+
)}
- {
e.stopPropagation();
if (isCreatingFolder) {
@@ -336,12 +345,13 @@ export default function MoveToFolderModal({
setModalState('INACTIVE');
}
}}
- className="dark:text-foreground hover:bg-accent dark:hover:bg-accent cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium"
+ className="dark:text-foreground h-auto rounded-3xl px-5 py-2 text-sm font-medium"
>
{t('cancel')}
-
+
{isCreatingFolder ? (
- {
e.stopPropagation();
if (newFolderName.trim()) {
@@ -351,24 +361,25 @@ export default function MoveToFolderModal({
}
}}
disabled={!newFolderName.trim()}
- className="bg-primary hover:bg-primary/90 rounded-3xl px-5 py-2 text-sm text-white disabled:opacity-50"
+ className="h-auto rounded-3xl px-5 py-2 text-sm text-white"
>
{t('agents.folders.createFolder')}
-
+
) : (
- {
e.stopPropagation();
handleMove();
}}
- className="bg-primary hover:bg-primary/90 rounded-3xl px-5 py-2 text-sm text-white disabled:opacity-50"
+ className="h-auto rounded-3xl px-5 py-2 text-sm text-white"
>
{t('agents.folders.move')}
-
+
)}
-
+
);
}
diff --git a/frontend/src/modals/SearchConversationsModal.tsx b/frontend/src/modals/SearchConversationsModal.tsx
index 72071155..830b4b6d 100644
--- a/frontend/src/modals/SearchConversationsModal.tsx
+++ b/frontend/src/modals/SearchConversationsModal.tsx
@@ -8,8 +8,10 @@ import {
import { useTranslation } from 'react-i18next';
import SearchIcon from '../assets/search.svg';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Modal } from '../components/ui/modal';
import { searchConversations } from '../preferences/preferenceApi';
-import WrapperModal from './WrapperModal';
type ConversationListItem = {
id: string;
@@ -38,10 +40,7 @@ function HighlightedText({ text, query }: { text: string; query: string }) {
<>
{parts.map((part, idx) =>
part.toLowerCase() === trimmed.toLowerCase() ? (
-
+
{part}
) : (
@@ -150,22 +149,28 @@ export default function SearchConversationsModal({
!!query.trim() && !isSearching && visibleConversations.length === 0;
return (
- {
+ if (!open) close();
+ }}
+ hideTitle
+ title={t('modals.searchConversations.searchPlaceholder')}
+ showCloseButton={false}
+ className="w-[92vw] !max-w-xl !p-0"
contentClassName="max-h-[70vh]"
>
@@ -190,9 +195,10 @@ export default function SearchConversationsModal({
const isActive = index === activeIndex;
return (
- {
resultRefs.current[index] = element;
}}
@@ -200,7 +206,7 @@ export default function SearchConversationsModal({
onMouseEnter={() => setActiveIndex(index)}
role="option"
aria-selected={isActive}
- className={`text-foreground flex w-full flex-col items-start gap-0.5 px-5 py-2.5 text-left text-sm ${
+ className={`text-foreground flex h-auto w-full flex-col items-start gap-0.5 rounded-none px-5 py-2.5 text-left ${
isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent'
}`}
>
@@ -222,11 +228,11 @@ export default function SearchConversationsModal({
/>
)}
-
+
);
})}
-
+
);
}
diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx
index d9a065d4..b6242a4d 100644
--- a/frontend/src/modals/ShareConversationModal.tsx
+++ b/frontend/src/modals/ShareConversationModal.tsx
@@ -3,9 +3,17 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import conversationService from '../api/services/conversationService';
-import Spinner from '../assets/spinner.svg';
-import Dropdown from '../components/Dropdown';
-import ToggleSwitch from '../components/ToggleSwitch';
+import Spinner from '../components/Spinner';
+import { Button } from '../components/ui/button';
+import { Modal } from '../components/ui/modal';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '../components/ui/select';
+import { Switch } from '../components/ui/switch';
import { Doc } from '../models/misc';
import {
selectChunks,
@@ -14,7 +22,6 @@ import {
selectSourceDocs,
selectToken,
} from '../preferences/preferenceSlice';
-import WrapperModal from './WrapperModal';
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
@@ -96,36 +103,48 @@ export const ShareConversationModal = ({
};
return (
-
-
-
- {t('modals.shareConv.label')}
-
-
- {t('modals.shareConv.note')}
-
+ {
+ if (!open) close();
+ }}
+ size="xl"
+ title={t('modals.shareConv.label')}
+ description={t('modals.shareConv.note')}
+ contentClassName="!overflow-visible"
+ >
+
{t('modals.shareConv.option')}
-
{allowPrompt && (
-
- setSourcePath(selection)
- }
- options={extractDocPaths(sourceDocs ?? [])}
- size="w-full"
- rounded="xl"
- />
+
)}
@@ -133,30 +152,29 @@ export const ShareConversationModal = ({
{`${domain}/share/${identifier ?? '....'}`}
{status === 'fetched' ? (
- handleCopyKey(`${domain}/share/${identifier}`)}
>
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
-
+
) : (
- {
shareCoversationPublicly(allowPrompt);
}}
>
{t('modals.shareConv.create')}
- {status === 'loading' && (
-
- )}
-
+ {status === 'loading' && }
+
)}
-
+
);
};
diff --git a/frontend/src/modals/WrapperModal.tsx b/frontend/src/modals/WrapperModal.tsx
deleted file mode 100644
index a1708821..00000000
--- a/frontend/src/modals/WrapperModal.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React, { useEffect, useRef } from 'react';
-import { createPortal } from 'react-dom';
-
-import Exit from '../assets/exit.svg';
-
-type WrapperModalPropsType = {
- children: React.ReactNode;
- close: () => void;
- isPerformingTask?: boolean;
- className?: string;
- contentClassName?: string;
-};
-
-export default function WrapperModal({
- children,
- close,
- isPerformingTask = false,
- className = '',
- contentClassName = '',
-}: WrapperModalPropsType) {
- const modalRef = useRef (null);
-
- useEffect(() => {
- if (isPerformingTask) return;
-
- const handleClickOutside = (event: MouseEvent) => {
- const target = event.target as Node;
- if (
- (target as Element)?.closest?.(
- '[data-radix-popper-content-wrapper], [data-radix-select-viewport], [role="listbox"]',
- )
- )
- return;
- if (document.querySelector('[data-radix-select-content]')) return;
- if (modalRef.current && !modalRef.current.contains(target)) close();
- };
-
- const handleEscapePress = (event: KeyboardEvent) => {
- if (event.key === 'Escape') close();
- };
-
- document.addEventListener('mousedown', handleClickOutside);
- document.addEventListener('keydown', handleEscapePress);
-
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- document.removeEventListener('keydown', handleEscapePress);
- };
- }, [close, isPerformingTask]);
-
- const modalContent = (
- e.stopPropagation()}
- onMouseDown={(e: React.MouseEvent) => e.stopPropagation()}
- >
-
-
- {!isPerformingTask && (
-
-
-
- )}
-
- {children}
-
-
-
- );
-
- return createPortal(modalContent, document.body);
-}
diff --git a/frontend/src/modals/types/index.ts b/frontend/src/modals/types/index.ts
index 3eac4701..2922c00c 100644
--- a/frontend/src/modals/types/index.ts
+++ b/frontend/src/modals/types/index.ts
@@ -25,10 +25,3 @@ export type AvailableToolType = {
parameters: object;
}[];
};
-
-export type WrapperModalPropsType = {
- children?: React.ReactNode;
- isPerformingTask?: boolean;
- close: () => void;
- className?: string;
-};
diff --git a/frontend/src/notifications/ToolApprovalToast.tsx b/frontend/src/notifications/ToolApprovalToast.tsx
index 60653682..4e369f83 100644
--- a/frontend/src/notifications/ToolApprovalToast.tsx
+++ b/frontend/src/notifications/ToolApprovalToast.tsx
@@ -1,7 +1,9 @@
+import { X } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { useMatch, useNavigate } from 'react-router-dom';
import WarnIcon from '../assets/warn.svg';
+import { Button } from '../components/ui/button';
import type { RootState } from '../store';
import {
@@ -103,42 +105,22 @@ export default function ToolApprovalToast() {
({ eventId, conversationId }) => (
-
+
Tool approval needed
- dispatch(dismissToolApproval(eventId))}
- className="flex h-8 items-center justify-center p-0 text-black opacity-70 transition-opacity hover:opacity-100 dark:text-white"
+ className="h-8 w-8 p-0 text-black opacity-70 transition-opacity hover:bg-transparent hover:opacity-100 dark:text-white"
aria-label="Dismiss"
>
-
-
+
+
@@ -149,22 +131,23 @@ export default function ToolApprovalToast() {
aria-hidden="true"
/>
{conversationName(conversationId)}
- {
dispatch(dismissToolApproval(eventId));
navigate(`/c/${conversationId}`);
}}
- className="rounded-full bg-[#7D54D1] px-3 py-1 text-[12px] font-medium text-white shadow-sm hover:bg-[#6a45b8]"
+ className="h-auto rounded-full px-3 py-1 text-xs font-medium text-white shadow-sm"
>
Review
-
+
),
diff --git a/frontend/src/preferences/PromptsModal.tsx b/frontend/src/preferences/PromptsModal.tsx
index 552e6f48..ec1ec7ed 100644
--- a/frontend/src/preferences/PromptsModal.tsx
+++ b/frontend/src/preferences/PromptsModal.tsx
@@ -1,12 +1,19 @@
+import { ChevronDown } from 'lucide-react';
import { ActiveState } from '../models/misc';
-import Input from '../components/Input';
+import { Input } from '../components/ui/input';
import { Link } from 'react-router-dom';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
-import WrapperModal from '../modals/WrapperModal';
-import Dropdown from '../components/Dropdown';
+import { Modal } from '../components/ui/modal';
+import { Button } from '../components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '../components/ui/dropdown-menu';
import BookIcon from '../assets/book.svg';
import userService from '../api/services/userService';
import { selectToken } from '../preferences/preferenceSlice';
@@ -90,6 +97,79 @@ const buildSystemVariableOptions = (translate: (key: string) => string) =>
label: translate(labelKey),
}));
+type VariableMenuProps = {
+ options: { label: string; value: string }[];
+ label: string;
+ textareaId: string;
+ content: string;
+ setContent: (content: string) => void;
+ triggerClassName?: string;
+ contentClassName?: string;
+};
+
+function VariableMenu({
+ options,
+ label,
+ textareaId,
+ content,
+ setContent,
+ triggerClassName,
+ contentClassName,
+}: VariableMenuProps) {
+ const handleSelect = (value: string) => {
+ const textarea = document.getElementById(textareaId) as HTMLTextAreaElement;
+ if (!textarea) return;
+ const cursorPosition = textarea.selectionStart;
+ const textBefore = content.slice(0, cursorPosition);
+ const textAfter = content.slice(cursorPosition);
+
+ // Add leading space if needed
+ const needsSpace =
+ cursorPosition > 0 && content.charAt(cursorPosition - 1) !== ' ';
+
+ const newText =
+ textBefore + (needsSpace ? ' ' : '') + `{{ ${value} }}` + textAfter;
+ setContent(newText);
+
+ setTimeout(() => {
+ textarea.focus();
+ const insertedLen = value.length + 6 + (needsSpace ? 1 : 0);
+ textarea.setSelectionRange(
+ cursorPosition + insertedLen,
+ cursorPosition + insertedLen,
+ );
+ }, 0);
+ };
+
+ return (
+
+
+
+ {label}
+
+
+
+
+ {options.map((opt) => (
+ handleSelect(opt.value)}
+ >
+ {opt.label}
+
+ ))}
+
+
+ );
+}
+
type PromptTextareaProps = {
id: string;
value: string;
@@ -134,7 +214,7 @@ function PromptTextarea({
|
- {
setTool({
...tool,
@@ -641,14 +645,14 @@ export default function ToolConfig({
),
});
}}
- >
+ />
|
- {
setTool({
...tool,
@@ -679,7 +683,7 @@ export default function ToolConfig({
),
});
}}
- >
+ />
|