mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-23 00:46:42 +08:00
Added some React IDs for playwright e2e tests (#13265)
### What problem does this PR solve? Necessary ids for implementing the new testing suite with playwright for UI ### Type of change - [x] Other (please describe): Testing IDs Co-authored-by: Liu An <asiro@qq.com>
This commit is contained in:
@@ -79,8 +79,9 @@ export const EmptyAppCard = (props: {
|
||||
isSearch?: boolean;
|
||||
size?: 'small' | 'large';
|
||||
children?: React.ReactNode;
|
||||
testId?: string;
|
||||
}) => {
|
||||
const { type, showIcon, className, isSearch, children } = props;
|
||||
const { type, showIcon, className, isSearch, children, testId } = props;
|
||||
let defaultClass = '';
|
||||
let style = {};
|
||||
switch (props.size) {
|
||||
@@ -97,7 +98,7 @@ export const EmptyAppCard = (props: {
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div onClick={isSearch ? undefined : props.onClick}>
|
||||
<div onClick={isSearch ? undefined : props.onClick} data-testid={testId}>
|
||||
<EmptyCard
|
||||
icon={showIcon ? EmptyCardData[type].icon : undefined}
|
||||
title={
|
||||
|
||||
@@ -65,6 +65,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
|
||||
>
|
||||
{(field) => (
|
||||
<Switch
|
||||
data-testid="parse-on-creation-toggle"
|
||||
onCheckedChange={field.onChange}
|
||||
checked={field.value}
|
||||
></Switch>
|
||||
@@ -77,6 +78,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
accept={{ '*': [] }}
|
||||
data-testid="dataset-upload-dropzone"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
@@ -97,7 +99,7 @@ export function FileUploadDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="dataset-upload-modal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -14,6 +14,7 @@ interface IProps {
|
||||
moreDropdown: React.ReactNode;
|
||||
sharedBadge?: ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
export function HomeCard({
|
||||
data,
|
||||
@@ -21,9 +22,12 @@ export function HomeCard({
|
||||
moreDropdown,
|
||||
sharedBadge,
|
||||
icon,
|
||||
testId,
|
||||
}: IProps) {
|
||||
return (
|
||||
<Card
|
||||
data-testid={testId}
|
||||
data-agent-name={data.name}
|
||||
onClick={() => {
|
||||
// navigateToSearch(data?.id);
|
||||
onClick?.();
|
||||
@@ -40,7 +44,7 @@ export function HomeCard({
|
||||
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
|
||||
<section className="flex justify-between">
|
||||
<section className="flex flex-1 min-w-0 gap-1 items-center">
|
||||
<div className="text-base font-bold leading-snug truncate">
|
||||
<div className="text-base font-bold leading-snug truncate" data-testid="agent-name">
|
||||
{data.name}
|
||||
</div>
|
||||
{icon}
|
||||
|
||||
@@ -122,6 +122,7 @@ export function KnowledgeBaseFormField({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
data-testid="chat-datasets-combobox"
|
||||
options={options}
|
||||
onValueChange={(value) => {
|
||||
handleDatasetSelectChange(value, field.onChange);
|
||||
@@ -131,6 +132,8 @@ export function KnowledgeBaseFormField({
|
||||
maxCount={100}
|
||||
defaultValue={field.value}
|
||||
showSelectAll={false}
|
||||
popoverTestId="datasets-options"
|
||||
optionTestIdPrefix="datasets"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -211,6 +211,7 @@ export function NextMessageInput({
|
||||
</FileUploadList>
|
||||
|
||||
<Textarea
|
||||
data-testid="chat-textarea"
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
placeholder={t('chat.messagePlaceholder')}
|
||||
@@ -267,7 +268,7 @@ export function NextMessageInput({
|
||||
</div>
|
||||
|
||||
{sendLoading ? (
|
||||
<Button onClick={stopOutputMessage} size="icon-xs">
|
||||
<Button data-testid="chat-stream-status" onClick={stopOutputMessage} size="icon-xs">
|
||||
<CircleStop />
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -48,6 +48,7 @@ export type SelectWithSearchFlagProps = {
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
emptyData?: string;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
function findLabelWithoutOptions(
|
||||
@@ -80,6 +81,7 @@ export const SelectWithSearch = forwardRef<
|
||||
disabled = false,
|
||||
placeholder = t('common.selectPlaceholder'),
|
||||
emptyData = t('common.noDataFound'),
|
||||
testId,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -154,6 +156,7 @@ export const SelectWithSearch = forwardRef<
|
||||
aria-expanded={open}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
'!bg-bg-input hover:bg-background border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto group',
|
||||
triggerClassName,
|
||||
@@ -213,6 +216,7 @@ export const SelectWithSearch = forwardRef<
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
onSelect={handleSelect}
|
||||
data-testid="combobox-option"
|
||||
className={
|
||||
value === option.value ? 'bg-bg-card' : ''
|
||||
}
|
||||
@@ -234,6 +238,7 @@ export const SelectWithSearch = forwardRef<
|
||||
value={group.value}
|
||||
disabled={group.disabled}
|
||||
onSelect={handleSelect}
|
||||
data-testid="combobox-option"
|
||||
className={cn('mb-1 min-h-10 ', {
|
||||
'bg-bg-card ': value === group.value,
|
||||
})}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function RenameDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogContent className="sm:max-w-[425px]" data-testid="rename-modal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title || t('common.rename')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -33,7 +33,7 @@ export function RenameDialog({
|
||||
onOk={onOk}
|
||||
></RenameForm>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
|
||||
<ButtonLoading data-testid="rename-save" type="submit" form={TagRenameId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -66,6 +66,7 @@ export function RenameForm({
|
||||
<FormLabel>{t('common.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
data-testid="rename-name-input"
|
||||
placeholder={t('common.namePlaceholder')}
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface ModalProps {
|
||||
onCancel?: () => void;
|
||||
okButtonClassName?: string;
|
||||
cancelButtonClassName?: string;
|
||||
testId?: string;
|
||||
okButtonTestId?: string;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
zIndex?: number;
|
||||
@@ -87,6 +89,8 @@ const Modal: ModalType = ({
|
||||
style,
|
||||
zIndex = 50,
|
||||
type,
|
||||
testId,
|
||||
okButtonTestId,
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
small: 'max-w-md',
|
||||
@@ -152,6 +156,7 @@ const Modal: ModalType = ({
|
||||
type="button"
|
||||
disabled={confirmLoading || disabled}
|
||||
onClick={() => handleOk()}
|
||||
data-testid={okButtonTestId}
|
||||
className={cn(
|
||||
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
|
||||
{ 'cursor-not-allowed': disabled },
|
||||
@@ -190,6 +195,7 @@ const Modal: ModalType = ({
|
||||
okButtonClassName,
|
||||
cancelButtonClassName,
|
||||
showCancel,
|
||||
okButtonTestId,
|
||||
]);
|
||||
|
||||
const contentEl = useMemo(() => {
|
||||
@@ -224,6 +230,7 @@ const Modal: ModalType = ({
|
||||
`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`,
|
||||
{ 'pt-10': closable && !title && !type },
|
||||
)}
|
||||
data-testid={testId}
|
||||
style={style}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
@@ -49,10 +49,12 @@ function MultiCommandItem({
|
||||
option,
|
||||
isSelected,
|
||||
toggleOption,
|
||||
optionTestId,
|
||||
}: {
|
||||
option: MultiSelectOptionType;
|
||||
isSelected: boolean;
|
||||
toggleOption(value: string): void;
|
||||
optionTestId?: string;
|
||||
}) {
|
||||
return (
|
||||
<CommandItem
|
||||
@@ -61,6 +63,7 @@ function MultiCommandItem({
|
||||
if (option.disabled) return false;
|
||||
toggleOption(option.value);
|
||||
}}
|
||||
data-testid={optionTestId}
|
||||
className={cn('cursor-pointer', {
|
||||
'cursor-not-allowed text-text-disabled': option.disabled,
|
||||
})}
|
||||
@@ -183,6 +186,8 @@ interface MultiSelectProps
|
||||
* If true, renders the multi-select component with a select all option.
|
||||
*/
|
||||
showSelectAll?: boolean;
|
||||
popoverTestId?: string;
|
||||
optionTestIdPrefix?: string;
|
||||
}
|
||||
|
||||
export const MultiSelect = React.forwardRef<
|
||||
@@ -202,6 +207,8 @@ export const MultiSelect = React.forwardRef<
|
||||
// asChild = false,
|
||||
className,
|
||||
showSelectAll = true,
|
||||
popoverTestId,
|
||||
optionTestIdPrefix,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -378,6 +385,7 @@ export const MultiSelect = React.forwardRef<
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
data-testid={popoverTestId}
|
||||
>
|
||||
<Command className="p-5 pb-8">
|
||||
{options && options.length > 0 && (
|
||||
@@ -410,7 +418,7 @@ export const MultiSelect = React.forwardRef<
|
||||
)}
|
||||
{!options.some((x) => 'options' in x) &&
|
||||
(options as unknown as MultiSelectOptionType[]).map(
|
||||
(option) => {
|
||||
(option, idx) => {
|
||||
const isSelected = selectedValues.includes(option.value);
|
||||
return (
|
||||
<MultiCommandItem
|
||||
@@ -418,6 +426,7 @@ export const MultiSelect = React.forwardRef<
|
||||
key={option.value}
|
||||
isSelected={isSelected}
|
||||
toggleOption={toggleOption}
|
||||
optionTestId={optionTestIdPrefix ? `${optionTestIdPrefix}-option-${idx}` : undefined}
|
||||
></MultiCommandItem>
|
||||
);
|
||||
},
|
||||
@@ -426,7 +435,7 @@ export const MultiSelect = React.forwardRef<
|
||||
{options.every((x) => 'options' in x) &&
|
||||
options.map((x, idx) => (
|
||||
<CommandGroup heading={x.label} key={idx}>
|
||||
{x.options.map((option) => {
|
||||
{x.options.map((option, optIdx) => {
|
||||
const isSelected = selectedValues.includes(option.value);
|
||||
|
||||
return (
|
||||
@@ -435,6 +444,7 @@ export const MultiSelect = React.forwardRef<
|
||||
key={option.value}
|
||||
isSelected={isSelected}
|
||||
toggleOption={toggleOption}
|
||||
optionTestId={optionTestIdPrefix ? `${optionTestIdPrefix}-option-${optIdx}` : undefined}
|
||||
></MultiCommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -94,7 +94,19 @@ export function Header() {
|
||||
tag.path === Routes.Root ? (
|
||||
<HeaderIcon className="size-6"></HeaderIcon>
|
||||
) : (
|
||||
<span>{tag.name}</span>
|
||||
<span
|
||||
data-testid={
|
||||
tag.path === Routes.Chats
|
||||
? 'nav-chat'
|
||||
: tag.path === Routes.Searches
|
||||
? 'nav-search'
|
||||
: tag.path === Routes.Agents
|
||||
? 'nav-agent'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
),
|
||||
value: tag.path,
|
||||
};
|
||||
@@ -128,7 +140,7 @@ export function Header() {
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<section className="py-5 px-10 flex justify-between items-center ">
|
||||
<section className="py-5 px-10 flex justify-between items-center " data-testid="top-nav">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={'/logo.svg'}
|
||||
@@ -146,7 +158,7 @@ export function Header() {
|
||||
onChange={handleChange}
|
||||
activeClassName="text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2"
|
||||
></Segmented>
|
||||
<div className="flex items-center gap-5 text-text-badge">
|
||||
<div className="flex items-center gap-5 text-text-badge" data-testid="auth-status">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://discord.com/invite/NjYzJD3GM3"
|
||||
@@ -183,7 +195,7 @@ export function Header() {
|
||||
{theme === 'light' ? <Sun /> : <Moon />}
|
||||
</Button>
|
||||
<BellButton></BellButton>
|
||||
<div className="relative">
|
||||
<div className="relative" data-testid="settings-entrypoint">
|
||||
<RAGFlowAvatar
|
||||
name={nickname}
|
||||
avatar={avatar}
|
||||
|
||||
@@ -71,6 +71,7 @@ function AgentChatBox() {
|
||||
<section className="flex flex-1 flex-col px-5 min-h-0 pb-4">
|
||||
<div className="flex-1 overflow-auto" ref={messageContainerRef}>
|
||||
<div>
|
||||
{!sendLoading && <div data-testid="agent-run-idle" />}
|
||||
{/* <Spin spinning={sendLoading}> */}
|
||||
{derivedMessages?.map((message, i) => {
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ export function ChatSheet({ hideModal }: IModalProps<any>) {
|
||||
return (
|
||||
<Sheet open modal={false} onOpenChange={hideModal}>
|
||||
<SheetContent
|
||||
data-testid="agent-run-chat"
|
||||
className={cn('top-20 bottom-0 p-0 flex flex-col h-auto')}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function Agent() {
|
||||
} = useRunDataflow({ showLogSheet: showPipelineLogSheet, setMessageId });
|
||||
|
||||
return (
|
||||
<section className="h-full">
|
||||
<section className="h-full" data-testid="agent-detail">
|
||||
<PageHeader>
|
||||
<section>
|
||||
<Breadcrumb>
|
||||
@@ -245,7 +245,7 @@ export default function Agent() {
|
||||
>
|
||||
<MessageSquareCode /> {t('flow.conversationVariable')}
|
||||
</ButtonLoading>
|
||||
<Button variant={'secondary'} onClick={handleButtonRunClick}>
|
||||
<Button data-testid="agent-run" variant={'secondary'} onClick={handleButtonRunClick}>
|
||||
<CirclePlay />
|
||||
{t('flow.run')}
|
||||
</Button>
|
||||
|
||||
@@ -18,6 +18,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
||||
|
||||
return (
|
||||
<HomeCard
|
||||
testId="agent-card"
|
||||
data={{ ...data, name: data.title, description: data.description || '' }}
|
||||
moreDropdown={
|
||||
<AgentDropdown showAgentRenameModal={showAgentRenameModal} agent={data}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function CreateAgentDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="agent-create-modal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('flow.createGraph')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -32,7 +32,7 @@ export function CreateAgentDialog({
|
||||
shouldChooseAgent={shouldChooseAgent}
|
||||
></CreateAgentForm>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
|
||||
<ButtonLoading data-testid="agent-save" type="submit" form={TagRenameId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -93,6 +93,7 @@ export default function Agents() {
|
||||
>
|
||||
<div className="flex flex-col gap-y-5 text-text-secondary text-sm pt-5">
|
||||
<div
|
||||
data-testid="agents-empty-create"
|
||||
className="flex items-center gap-x-2 hover:text-text-primary cursor-pointer"
|
||||
onClick={showCreatingModal}
|
||||
>
|
||||
@@ -117,7 +118,7 @@ export default function Agents() {
|
||||
</EmptyAppCard>
|
||||
</div>
|
||||
)}
|
||||
<section className="flex flex-col w-full flex-1">
|
||||
<section className="flex flex-col w-full flex-1" data-testid="agents-list">
|
||||
{(!!data?.length || searchString) && (
|
||||
<>
|
||||
<div className="px-8 pt-8 ">
|
||||
@@ -131,13 +132,13 @@ export default function Agents() {
|
||||
value={filterValue}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger data-testid="create-agent">
|
||||
<Button>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('flow.createGraph')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContent data-testid="agent-create-menu">
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={showCreatingModal}
|
||||
@@ -153,6 +154,7 @@ export default function Agents() {
|
||||
{t('flow.createFromTemplate')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="agent-import-json"
|
||||
justifyBetween={false}
|
||||
onClick={handleImportJson}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function NameFormField() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<RAGFlowFormItem name="name" required label={t('common.name')}>
|
||||
<Input placeholder={t('common.namePlaceholder')} autoComplete="off" />
|
||||
<Input data-testid="agent-name-input" placeholder={t('common.namePlaceholder')} autoComplete="off" />
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ export function UploadAgentDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="agent-import-modal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<UploadAgentForm hideModal={hideModal} onOk={onOk}></UploadAgentForm>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
|
||||
<ButtonLoading data-testid="agent-import-save" type="submit" form={TagRenameId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -53,6 +53,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
|
||||
<FormLabel required>DSL</FormLabel>
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
data-testid="agent-import-file"
|
||||
className="text-ellipsis overflow-hidden"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
|
||||
@@ -105,6 +105,7 @@ export function DatasetActionCell({
|
||||
)}
|
||||
<ConfirmDeleteDialog onOk={handleRemove}>
|
||||
<Button
|
||||
data-testid="document-delete"
|
||||
variant="transparent"
|
||||
className="border-none hover:bg-bg-card text-text-primary"
|
||||
size={'sm'}
|
||||
|
||||
@@ -151,6 +151,8 @@ export function DatasetTable({
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-testid="document-row"
|
||||
data-doc-name={row.original.name}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="group"
|
||||
>
|
||||
|
||||
@@ -40,6 +40,15 @@ const IconMap = {
|
||||
),
|
||||
};
|
||||
|
||||
const ParseStatusStateMap = {
|
||||
[RunningStatus.UNSTART]: 'unstart',
|
||||
[RunningStatus.RUNNING]: 'running',
|
||||
[RunningStatus.CANCEL]: 'cancel',
|
||||
[RunningStatus.DONE]: 'success',
|
||||
[RunningStatus.FAIL]: 'fail',
|
||||
[RunningStatus.SCHEDULE]: 'running',
|
||||
} as const;
|
||||
|
||||
export function ParsingStatusCell({
|
||||
record,
|
||||
showChangeParserModal,
|
||||
@@ -89,7 +98,11 @@ export function ParsingStatusCell({
|
||||
showLog(record);
|
||||
};
|
||||
return (
|
||||
<section className="flex gap-8 items-center">
|
||||
<section
|
||||
className="flex gap-8 items-center"
|
||||
data-testid="document-parse-status"
|
||||
data-state={ParseStatusStateMap[run] ?? 'unknown'}
|
||||
>
|
||||
<div className="text-ellipsis w-[100px] flex items-center justify-between">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
import './index.less';
|
||||
|
||||
export const FlipFaceContext = createContext<'front' | 'back'>('front');
|
||||
|
||||
type IProps = {
|
||||
children: React.ReactNode;
|
||||
isLoginPage: boolean;
|
||||
@@ -32,18 +34,34 @@ const FlipCard3D = (props: IProps) => {
|
||||
className={`relative w-full h-full transition-transform transform-style-3d ${isFlipped ? 'rotate-y-180' : ''}`}
|
||||
>
|
||||
{/* Front Face */}
|
||||
<div className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-0">
|
||||
{children}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-0"
|
||||
{...(!isFlipped ? { 'data-testid': 'auth-card-active' } : {})}
|
||||
>
|
||||
<FlipFaceContext.Provider value="front">
|
||||
{children}
|
||||
</FlipFaceContext.Provider>
|
||||
</div>
|
||||
|
||||
{/* Back Face */}
|
||||
<div className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-180">
|
||||
{children}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-180"
|
||||
{...(isFlipped ? { 'data-testid': 'auth-card-active' } : {})}
|
||||
>
|
||||
<FlipFaceContext.Provider value="back">
|
||||
{children}
|
||||
</FlipFaceContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isBackfaceVisibilitySupported() && <>{children}</>}
|
||||
{!isBackfaceVisibilitySupported() && (
|
||||
<div data-testid="auth-card-active">
|
||||
<FlipFaceContext.Provider value={isFlipped ? 'back' : 'front'}>
|
||||
{children}
|
||||
</FlipFaceContext.Provider>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/hooks/use-login-request';
|
||||
import { useSystemConfig } from '@/hooks/use-system-request';
|
||||
import { rsaPsw } from '@/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
@@ -29,9 +29,214 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { BgSvg } from './bg';
|
||||
import FlipCard3D from './card';
|
||||
import FlipCard3D, { FlipFaceContext } from './card';
|
||||
import './index.less';
|
||||
|
||||
type LoginFormContentProps = {
|
||||
isLoginPage: boolean;
|
||||
title: string;
|
||||
form: ReturnType<typeof useForm>;
|
||||
loading: boolean;
|
||||
onCheck: (params: any) => Promise<void>;
|
||||
changeTitle: () => void;
|
||||
registerEnabled: boolean;
|
||||
channels: { channel: string; icon?: string; display_name: string }[];
|
||||
handleLoginWithChannel: (channel: string) => void;
|
||||
t: ReturnType<typeof useTranslation>['t'];
|
||||
};
|
||||
|
||||
function LoginFormContent({
|
||||
isLoginPage,
|
||||
title,
|
||||
form,
|
||||
loading,
|
||||
onCheck,
|
||||
changeTitle,
|
||||
registerEnabled,
|
||||
channels,
|
||||
handleLoginWithChannel,
|
||||
t,
|
||||
}: LoginFormContentProps) {
|
||||
const face = useContext(FlipFaceContext);
|
||||
const isActiveFace = isLoginPage ? face === 'front' : face === 'back';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-8 text-text-primary "
|
||||
data-testid="auth-form"
|
||||
data-active={isActiveFace ? 'true' : undefined}
|
||||
onSubmit={form.handleSubmit(onCheck)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
data-testid="auth-email"
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{title === 'register' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('nicknameLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
data-testid="auth-nickname"
|
||||
placeholder={t('nicknamePlaceholder')}
|
||||
autoComplete="username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
data-testid="auth-password"
|
||||
type={'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete={
|
||||
title === 'login'
|
||||
? 'current-password'
|
||||
: 'new-password'
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{title === 'login' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
<FormLabel
|
||||
className={cn(' hover:text-text-primary', {
|
||||
'text-text-disabled': !field.value,
|
||||
'text-text-primary': field.value,
|
||||
})}
|
||||
>
|
||||
{t('rememberMe')}
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ButtonLoading
|
||||
data-testid="auth-submit"
|
||||
type="submit"
|
||||
loading={loading}
|
||||
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
|
||||
>
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</ButtonLoading>
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className="mt-3 border">
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{title === 'login' && registerEnabled && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signInTip')}
|
||||
<Button
|
||||
data-testid="auth-toggle-register"
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'register' && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signUpTip')}
|
||||
<Button
|
||||
data-testid="auth-toggle-login"
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('login')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Login = () => {
|
||||
const [title, setTitle] = useState('login');
|
||||
const navigate = useNavigate();
|
||||
@@ -174,182 +379,18 @@ const Login = () => {
|
||||
|
||||
{/* Login Form */}
|
||||
<FlipCard3D isLoginPage={isLoginPage}>
|
||||
<div className="flex flex-col items-center justify-center w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-8 text-text-primary "
|
||||
onSubmit={form.handleSubmit(onCheck)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{title === 'register' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('nicknameLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('nicknamePlaceholder')}
|
||||
autoComplete="username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete={
|
||||
title === 'login'
|
||||
? 'current-password'
|
||||
: 'new-password'
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
{/* <button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button> */}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{title === 'login' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
<FormLabel
|
||||
className={cn(' hover:text-text-primary', {
|
||||
'text-text-disabled': !field.value,
|
||||
'text-text-primary': field.value,
|
||||
})}
|
||||
>
|
||||
{t('rememberMe')}
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
loading={loading}
|
||||
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
|
||||
>
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</ButtonLoading>
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className="mt-3 border">
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{title === 'login' && registerEnabled && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signInTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'register' && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signUpTip')}
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
onClick={changeTitle}
|
||||
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
|
||||
>
|
||||
{t('login')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<LoginFormContent
|
||||
isLoginPage={isLoginPage}
|
||||
title={title}
|
||||
form={form}
|
||||
loading={loading}
|
||||
onCheck={onCheck}
|
||||
changeTitle={changeTitle}
|
||||
registerEnabled={registerEnabled}
|
||||
channels={channels || []}
|
||||
handleLoginWithChannel={handleLoginWithChannel}
|
||||
t={t}
|
||||
/>
|
||||
</FlipCard3D>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ export function SavingButton({ loading }: SaveButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonLoading type="submit" loading={loading}>
|
||||
<ButtonLoading data-testid="chat-settings-save" type="submit" loading={loading}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function Chat() {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="h-full flex flex-col">
|
||||
<section className="h-full flex flex-col" data-testid="chat-detail">
|
||||
<PageHeader>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function ChatList() {
|
||||
}, [isCreate, handleShowCreateModal, searchParams, setSearchParams]);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col w-full flex-1">
|
||||
<section className="flex flex-col w-full flex-1" data-testid="chats-list">
|
||||
{data.dialogs?.length <= 0 && !searchString && (
|
||||
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
|
||||
<EmptyAppCard
|
||||
@@ -59,6 +59,7 @@ export default function ChatList() {
|
||||
isSearch={!!searchString}
|
||||
type={EmptyCardType.Chat}
|
||||
onClick={() => handleShowCreateModal()}
|
||||
testId="chats-empty-create"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -71,7 +72,7 @@ export default function ChatList() {
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
>
|
||||
<Button onClick={handleShowCreateModal}>
|
||||
<Button onClick={handleShowCreateModal} data-testid="create-chat">
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('chat.createChat')}
|
||||
</Button>
|
||||
@@ -86,6 +87,7 @@ export default function ChatList() {
|
||||
isSearch={!!searchString}
|
||||
type={EmptyCardType.Chat}
|
||||
onClick={() => handleShowCreateModal()}
|
||||
testId="chats-empty-create"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function SearchPage() {
|
||||
}, [isSearching]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<section data-testid="search-detail">
|
||||
<PageHeader>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
|
||||
@@ -391,6 +391,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
||||
</FormLabel>
|
||||
<FormControl className="bg-bg-input">
|
||||
<MultiSelect
|
||||
data-testid="search-datasets-combobox"
|
||||
options={datasetList}
|
||||
onValueChange={(value) => {
|
||||
handleDatasetSelectChange(value, field.onChange);
|
||||
@@ -399,6 +400,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
maxCount={10}
|
||||
defaultValue={field.value}
|
||||
popoverTestId="datasets-options"
|
||||
optionTestIdPrefix="datasets"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -583,7 +586,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
||||
>
|
||||
{t('search.cancelText')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={formSubmitLoading}>
|
||||
<Button data-testid="search-settings-save" type="submit" disabled={formSubmitLoading}>
|
||||
{formSubmitLoading && (
|
||||
<div className="size-4">
|
||||
<Spin size="small" />
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function SearchList() {
|
||||
}, [isCreate, openCreateModalFun, searchUrl, setSearchUrl]);
|
||||
|
||||
return (
|
||||
<section className="w-full h-full flex flex-col">
|
||||
<section className="w-full h-full flex flex-col" data-testid="search-list">
|
||||
{(!list?.data?.search_apps?.length ||
|
||||
list?.data?.search_apps?.length <= 0) &&
|
||||
!searchString && (
|
||||
@@ -77,6 +77,7 @@ export default function SearchList() {
|
||||
type={EmptyCardType.Search}
|
||||
isSearch={!!searchString}
|
||||
onClick={() => openCreateModalFun()}
|
||||
testId="search-empty-create"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -92,6 +93,7 @@ export default function SearchList() {
|
||||
>
|
||||
<Button
|
||||
variant={'default'}
|
||||
data-testid="create-search"
|
||||
onClick={() => {
|
||||
openCreateModalFun();
|
||||
}}
|
||||
@@ -112,6 +114,7 @@ export default function SearchList() {
|
||||
type={EmptyCardType.Search}
|
||||
isSearch={!!searchString}
|
||||
onClick={() => openCreateModalFun()}
|
||||
testId="search-empty-create"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full rounded-lg border border-border-button`}>
|
||||
<div className={`w-full rounded-lg border border-border-button`} data-testid="added-model-card" data-provider={item.name}>
|
||||
{/* Header */}
|
||||
<div className="flex h-16 items-center justify-between p-4 cursor-pointer transition-colors text-text-secondary">
|
||||
<div className="flex items-center space-x-3">
|
||||
|
||||
@@ -73,6 +73,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
|
||||
value: formData.llm_id,
|
||||
options: modelOptions as SelectWithSearchFlagOptionType[],
|
||||
tooltip: t('chatModelTip'),
|
||||
testId: 'default-llm-combobox',
|
||||
},
|
||||
{
|
||||
id: 'embd_id',
|
||||
@@ -82,6 +83,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
|
||||
LlmModelType.Embedding
|
||||
] as SelectWithSearchFlagOptionType[],
|
||||
tooltip: t('embeddingModelTip'),
|
||||
testId: 'default-embedding-combobox',
|
||||
},
|
||||
{
|
||||
id: 'img2txt_id',
|
||||
@@ -129,6 +131,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
|
||||
tooltip,
|
||||
id,
|
||||
isRequired,
|
||||
testId,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -136,6 +139,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
|
||||
options: SelectWithSearchFlagOptionType[];
|
||||
tooltip?: string;
|
||||
isRequired?: boolean;
|
||||
testId?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
@@ -162,6 +166,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
|
||||
onChange={(value) => handleFieldChange(id, value)}
|
||||
placeholder={t('selectModelPlaceholder')}
|
||||
emptyData={t('modelEmptyTip')}
|
||||
testId={testId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@ export const AvailableModels: FC<{
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" text-text-primary h-full p-4">
|
||||
<div className=" text-text-primary h-full p-4" data-testid="available-models-section">
|
||||
<div className="text-text-primary text-base mb-4">
|
||||
{t('availableModels')}
|
||||
</div>
|
||||
@@ -87,6 +87,7 @@ export const AvailableModels: FC<{
|
||||
<div className="mb-6">
|
||||
{/* <div className="relative"> */}
|
||||
<SearchInput
|
||||
data-testid="model-providers-search"
|
||||
type="text"
|
||||
placeholder={t('search')}
|
||||
value={searchTerm}
|
||||
@@ -131,6 +132,8 @@ export const AvailableModels: FC<{
|
||||
{filteredModels.map((model) => (
|
||||
<div
|
||||
key={model.name}
|
||||
data-testid="available-model-card"
|
||||
data-provider={model.name}
|
||||
className=" border border-border-button rounded-lg p-3 hover:bg-bg-input transition-colors group"
|
||||
onClick={() => handleAddModel(model.name)}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const UsedModel = ({
|
||||
}) => {
|
||||
const { myLlmList: llmList } = useSelectLlmList();
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-5 mb-4">
|
||||
<div className="flex flex-col w-full gap-5 mb-4" data-testid="added-models-section">
|
||||
<div className="text-text-primary text-2xl font-medium mb-2 mt-4">
|
||||
{t('setting.addedModels')}
|
||||
</div>
|
||||
|
||||
@@ -86,6 +86,8 @@ const ApiKeyModal = ({
|
||||
okText={t('save')}
|
||||
cancelText={t('cancel')}
|
||||
className="!w-[600px]"
|
||||
testId="apikey-modal"
|
||||
okButtonTestId="apikey-save"
|
||||
>
|
||||
<Form {...form}>
|
||||
<div className="space-y-4 py-4">
|
||||
@@ -103,6 +105,7 @@ const ApiKeyModal = ({
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="apikey-input"
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
@@ -68,6 +68,7 @@ export function SideBar() {
|
||||
'bg-bg-card text-text-primary': active === item.key,
|
||||
'bg-bg-base text-text-secondary': active !== item.key,
|
||||
})}
|
||||
data-testid={item.key === Routes.Model ? 'settings-nav-model-providers' : undefined}
|
||||
onClick={handleMenuClick(item.key)}
|
||||
>
|
||||
<section className="flex items-center gap-2.5">
|
||||
|
||||
Reference in New Issue
Block a user