Compare commits

...

5 Commits

Author SHA1 Message Date
a0d5f81098 Feat: include author, journal name, volume, issue, page, and DOI in PubMed search results (#10481)
### What problem does this PR solve?

issue:
[#6571](https://github.com/infiniflow/ragflow/issues/6571)
change:
include author, journal name, volume, issue, page, and DOI in PubMed
search results

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-11 16:00:16 +08:00
52f26f4643 feat(login): Refactor the login page and add dynamic background and highlight effects #9869 (#10482)
### What problem does this PR solve?

Refactor(login): Refactor the login page and add dynamic background and
highlight effects #9869

### Type of change
- [x] Refactoring
2025-10-11 15:24:42 +08:00
313e92dd9b Fix: Fixed the issue where the connection lines of placeholder nodes in the agent canvas could not be displayed #9869 (#10485)
### What problem does this PR solve?

Fix: Fixed the issue where the connection lines of placeholder nodes in
the agent canvas could not be displayed #9869

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-11 15:24:27 +08:00
fee757eb41 Fix: Disable reasoning on Gemini 2.5 Flash by default (#10477)
### What problem does this PR solve?

Gemini 2.5 Flash Models use reasoning by default. There is currently no
way to disable this behaviour. This leads to very long response times (>
1min). The default behaviour should be, that reasoning is disabled and
configurable

issue #10474 

### Type of change

- [X] Bug Fix (non-breaking change which fixes an issue)
2025-10-11 10:22:51 +08:00
b5ddc7ca05 fix: return type annotation for get_urls() in download_deps (#10478)
### What problem does this PR solve?

Fixes the return type annotation for the `get_urls` function in
`download_deps.py`

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-11 09:49:09 +08:00
14 changed files with 672 additions and 446 deletions

View File

@ -85,13 +85,7 @@ class PubMed(ToolBase, ABC):
self._retrieve_chunks(pubmedcnt.findall("PubmedArticle"),
get_title=lambda child: child.find("MedlineCitation").find("Article").find("ArticleTitle").text,
get_url=lambda child: "https://pubmed.ncbi.nlm.nih.gov/" + child.find("MedlineCitation").find("PMID").text,
get_content=lambda child: child.find("MedlineCitation") \
.find("Article") \
.find("Abstract") \
.find("AbstractText").text \
if child.find("MedlineCitation")\
.find("Article").find("Abstract") \
else "No abstract available")
get_content=lambda child: self._format_pubmed_content(child),)
return self.output("formalized_content")
except Exception as e:
last_e = e
@ -104,5 +98,50 @@ class PubMed(ToolBase, ABC):
assert False, self.output()
def _format_pubmed_content(self, child):
"""Extract structured reference info from PubMed XML"""
def safe_find(path):
node = child
for p in path.split("/"):
if node is None:
return None
node = node.find(p)
return node.text if node is not None and node.text else None
title = safe_find("MedlineCitation/Article/ArticleTitle") or "No title"
abstract = safe_find("MedlineCitation/Article/Abstract/AbstractText") or "No abstract available"
journal = safe_find("MedlineCitation/Article/Journal/Title") or "Unknown Journal"
volume = safe_find("MedlineCitation/Article/Journal/JournalIssue/Volume") or "-"
issue = safe_find("MedlineCitation/Article/Journal/JournalIssue/Issue") or "-"
pages = safe_find("MedlineCitation/Article/Pagination/MedlinePgn") or "-"
# Authors
authors = []
for author in child.findall(".//AuthorList/Author"):
lastname = safe_find("LastName") or ""
forename = safe_find("ForeName") or ""
fullname = f"{forename} {lastname}".strip()
if fullname:
authors.append(fullname)
authors_str = ", ".join(authors) if authors else "Unknown Authors"
# DOI
doi = None
for eid in child.findall(".//ArticleId"):
if eid.attrib.get("IdType") == "doi":
doi = eid.text
break
return (
f"Title: {title}\n"
f"Authors: {authors_str}\n"
f"Journal: {journal}\n"
f"Volume: {volume}\n"
f"Issue: {issue}\n"
f"Pages: {pages}\n"
f"DOI: {doi or '-'}\n"
f"Abstract: {abstract.strip()}"
)
def thoughts(self) -> str:
return "Looking for scholarly papers on `{}`,” prioritising reputable sources.".format(self.get_input().get("query", "-_-!"))

View File

@ -16,7 +16,7 @@ import os
import urllib.request
import argparse
def get_urls(use_china_mirrors=False) -> Union[str, list[str]]:
def get_urls(use_china_mirrors=False) -> list[Union[str, list[str]]]:
if use_china_mirrors:
return [
"http://mirrors.tuna.tsinghua.edu.cn/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb",

View File

@ -1188,8 +1188,36 @@ class GoogleChat(Base):
del gen_conf[k]
return gen_conf
def _get_thinking_config(self, gen_conf):
"""Extract and create ThinkingConfig from gen_conf.
Default behavior for Vertex AI Generative Models: thinking_budget=0 (disabled)
unless explicitly specified by the user. This does not apply to Claude models.
Users can override by setting thinking_budget in gen_conf/llm_setting:
- 0: Disabled (default)
- 1-24576: Manual budget
- -1: Auto (model decides)
"""
# Claude models don't support ThinkingConfig
if "claude" in self.model_name:
gen_conf.pop("thinking_budget", None)
return None
# For Vertex AI Generative Models, default to thinking disabled
thinking_budget = gen_conf.pop("thinking_budget", 0)
if thinking_budget is not None:
try:
import vertexai.generative_models as glm # type: ignore
return glm.ThinkingConfig(thinking_budget=thinking_budget)
except Exception:
pass
return None
def _chat(self, history, gen_conf={}, **kwargs):
system = history[0]["content"] if history and history[0]["role"] == "system" else ""
thinking_config = self._get_thinking_config(gen_conf)
gen_conf = self._clean_conf(gen_conf)
if "claude" in self.model_name:
response = self.client.messages.create(
@ -1223,7 +1251,10 @@ class GoogleChat(Base):
}
]
response = self.client.generate_content(hist, generation_config=gen_conf)
if thinking_config:
response = self.client.generate_content(hist, generation_config=gen_conf, thinking_config=thinking_config)
else:
response = self.client.generate_content(hist, generation_config=gen_conf)
ans = response.text
return ans, response.usage_metadata.total_token_count
@ -1255,6 +1286,7 @@ class GoogleChat(Base):
response = None
total_tokens = 0
self.client._system_instruction = system
thinking_config = self._get_thinking_config(gen_conf)
if "max_tokens" in gen_conf:
gen_conf["max_output_tokens"] = gen_conf["max_tokens"]
del gen_conf["max_tokens"]
@ -1272,7 +1304,10 @@ class GoogleChat(Base):
]
ans = ""
try:
response = self.client.generate_content(history, generation_config=gen_conf, stream=True)
if thinking_config:
response = self.client.generate_content(history, generation_config=gen_conf, thinking_config=thinking_config, stream=True)
else:
response = self.client.generate_content(history, generation_config=gen_conf, stream=True)
for resp in response:
ans = resp.text
total_tokens += num_tokens_from_string(ans)

View File

@ -1619,12 +1619,12 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
tokenizerRequired: '请先添加Tokenizer节点',
tokenizerDescription:
'根据所选的搜索方法,将文本转换为所需的数据结构(例如,用于嵌入搜索的向量嵌入)。',
splitter: '分词器拆分器',
splitter: '按字符分割',
splitterDescription:
'根据分词器长度将文本拆分成块,并带有可选的分隔符和重叠。',
hierarchicalMergerDescription:
'使用正则表达式规则按标题层次结构将文档拆分成多个部分,以实现更精细的控制。',
hierarchicalMerger: '标题拆分器',
hierarchicalMerger: '标题分割',
extractor: '提取器',
extractorDescription:
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',

View File

@ -29,9 +29,8 @@ function InnerButtonEdge({
data,
sourceHandleId,
}: EdgeProps<Edge<{ isHovered: boolean }>>) {
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
const highlightedPlaceholderEdgeId = useGraphStore(
(state) => state.highlightedPlaceholderEdgeId,
const { deleteEdgeById, getOperatorTypeFromId } = useGraphStore(
(state) => state,
);
const [edgePath, labelX, labelY] = getBezierPath({
@ -48,12 +47,16 @@ function InnerButtonEdge({
: {};
}, [selected]);
const isTargetPlaceholder = useMemo(() => {
return getOperatorTypeFromId(target) === Operator.Placeholder;
}, [getOperatorTypeFromId, target]);
const placeholderHighlightStyle = useMemo(() => {
const isHighlighted = highlightedPlaceholderEdgeId === id;
const isHighlighted = isTargetPlaceholder;
return isHighlighted
? { strokeWidth: 2, stroke: 'var(--accent-primary)' }
? { strokeWidth: 2, stroke: 'rgb(var(--accent-primary))' }
: {};
}, [highlightedPlaceholderEdgeId, id]);
}, [isTargetPlaceholder]);
const onEdgeClick = () => {
deleteEdgeById(id);
@ -83,9 +86,10 @@ function InnerButtonEdge({
data?.isHovered &&
sourceHandleId !== NodeHandleId.Tool &&
sourceHandleId !== NodeHandleId.AgentBottom && // The connection between the agent node and the tool node does not need to display the delete button
!target.startsWith(Operator.Tool)
!target.startsWith(Operator.Tool) &&
!isTargetPlaceholder
);
}, [data?.isHovered, sourceHandleId, target]);
}, [data?.isHovered, isTargetPlaceholder, sourceHandleId, target]);
return (
<>

View File

@ -1,8 +1,7 @@
import { Connection, Position } from '@xyflow/react';
import { Connection, OnConnectEnd, Position } from '@xyflow/react';
import { useCallback, useRef } from 'react';
import { useDropdownManager } from '../canvas/context';
import { Operator, PREVENT_CLOSE_DELAY } from '../constant';
import useGraphStore from '../store';
import { useAddNode } from './use-add-node';
interface ConnectionStartParams {
@ -40,7 +39,6 @@ export const useConnectionDrag = (
const { addCanvasNode } = useAddNode(reactFlowInstance);
const { setActiveDropdown } = useDropdownManager();
const { setHighlightedPlaceholderEdgeId } = useGraphStore();
/**
* Connection start handler function
@ -66,8 +64,8 @@ export const useConnectionDrag = (
/**
* Connection end handler function
*/
const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent) => {
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
if ('clientX' in event && 'clientY' in event) {
const { clientX, clientY } = event;
setDropdownPosition({ x: clientX, y: clientY });
@ -113,11 +111,6 @@ export const useConnectionDrag = (
if (newNodeId) {
setCreatedPlaceholderRef(newNodeId);
if (connectionStartRef.current) {
const edgeId = `xy-edge__${connectionStartRef.current.nodeId}${connectionStartRef.current.handleId}-${newNodeId}end`;
setHighlightedPlaceholderEdgeId(edgeId);
}
}
// Calculate placeholder node position and display dropdown menu
@ -154,7 +147,6 @@ export const useConnectionDrag = (
calculateDropdownPosition,
setActiveDropdown,
showModal,
setHighlightedPlaceholderEdgeId,
checkAndRemoveExistingPlaceholder,
removePlaceholderNode,
hideModal,
@ -206,13 +198,7 @@ export const useConnectionDrag = (
removePlaceholderNode();
hideModal();
clearActiveDropdown();
setHighlightedPlaceholderEdgeId(null);
}, [
removePlaceholderNode,
hideModal,
clearActiveDropdown,
setHighlightedPlaceholderEdgeId,
]);
}, [removePlaceholderNode, hideModal, clearActiveDropdown]);
return {
onConnectStart,

View File

@ -42,9 +42,6 @@ export const usePlaceholderManager = (reactFlowInstance: any) => {
});
}
// Clear highlighted placeholder edge
useGraphStore.getState().setHighlightedPlaceholderEdgeId(null);
// Update ref reference
if (createdPlaceholderRef.current === existingPlaceholder.id) {
createdPlaceholderRef.current = null;
@ -62,8 +59,7 @@ export const usePlaceholderManager = (reactFlowInstance: any) => {
reactFlowInstance &&
!userSelectedNodeRef.current
) {
const { nodes, edges, setHighlightedPlaceholderEdgeId } =
useGraphStore.getState();
const { nodes, edges } = useGraphStore.getState();
// Remove edges related to placeholder
const edgesToRemove = edges.filter(
@ -84,8 +80,6 @@ export const usePlaceholderManager = (reactFlowInstance: any) => {
});
}
setHighlightedPlaceholderEdgeId(null);
createdPlaceholderRef.current = null;
}
@ -101,13 +95,7 @@ export const usePlaceholderManager = (reactFlowInstance: any) => {
(newNodeId: string) => {
// First establish connection between new node and source, then delete placeholder
if (createdPlaceholderRef.current && reactFlowInstance) {
const {
nodes,
edges,
addEdge,
updateNode,
setHighlightedPlaceholderEdgeId,
} = useGraphStore.getState();
const { nodes, edges, addEdge, updateNode } = useGraphStore.getState();
// Find placeholder node to get its position
const placeholderNode = nodes.find(
@ -157,8 +145,6 @@ export const usePlaceholderManager = (reactFlowInstance: any) => {
edges: edgesToRemove,
});
}
setHighlightedPlaceholderEdgeId(null);
}
// Mark that user has selected a node

View File

@ -39,7 +39,6 @@ export type RFState = {
selectedEdgeIds: string[];
clickedNodeId: string; // currently selected node
clickedToolId: string; // currently selected tool id
highlightedPlaceholderEdgeId: string | null;
onNodesChange: OnNodesChange<RAGFlowNodeType>;
onEdgesChange: OnEdgesChange;
onEdgeMouseEnter?: EdgeMouseHandler<Edge>;
@ -90,7 +89,6 @@ export type RFState = {
) => void; // Deleting a condition of a classification operator will delete the related edge
findAgentToolNodeById: (id: string | null) => string | undefined;
selectNodeIds: (nodeIds: string[]) => void;
setHighlightedPlaceholderEdgeId: (edgeId: string | null) => void;
};
// this is our useStore hook that we can use in our components to get parts of the store and call actions
@ -103,7 +101,6 @@ const useGraphStore = create<RFState>()(
selectedEdgeIds: [] as string[],
clickedNodeId: '',
clickedToolId: '',
highlightedPlaceholderEdgeId: null,
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
@ -530,9 +527,6 @@ const useGraphStore = create<RFState>()(
})),
);
},
setHighlightedPlaceholderEdgeId: (edgeId) => {
set({ highlightedPlaceholderEdgeId: edgeId });
},
})),
{ name: 'graph', trace: true },
),

View File

@ -0,0 +1,159 @@
import './index.less';
const aspectRatio = {
top: 240,
middle: 466,
bottom: 704,
};
export const BgSvg = () => {
const def = (
path: string,
id: number | string = '',
type: keyof typeof aspectRatio,
) => {
return (
<svg
className="w-full h-full"
// style={{ aspectRatio: `1440/${aspectRatio[type]}` }}
// preserveAspectRatio="xMinYMid meet"
preserveAspectRatio="none"
// viewBox={`${getPathBounds(path).minX} 0 ${
// getPathBounds(path).width
// } ${height}`}
viewBox={`0 0 1440 ${aspectRatio[type]}`}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id={`glow${id}`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#80FFF8" stopOpacity="0" />
<stop offset="50%" stopColor="#80FFF8" stopOpacity="1" />
<stop offset="100%" stopColor="#80FFF8" stopOpacity="0" />
</linearGradient>
<linearGradient
id="strokeWidthGradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop offset="0%" stopColor="#000" />
<stop offset="10%" stopColor="#fff" />
<stop offset="50%" stopColor="#fff" />
<stop offset="90%" stopColor="#fff" />
<stop offset="100%" stopColor="#000" />
</linearGradient>
<linearGradient
id={`highlight${id}`}
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop offset="45%" stopColor="#FFF" stopOpacity="0.2" />
<stop offset="48%" stopColor="#FFD700" stopOpacity="0.3" />
</linearGradient>
<filter
id={`glowFilter${id}`}
x="-10%"
y="-10%"
width="120%"
height="120%"
>
<feGaussianBlur in="SourceGraphic" stdDeviation="5.2" />
{/* <feBlend
in="blur"
in2="SourceGraphic"
mode="screen"
result="glow"
/> */}
</filter>
<filter
id={`highlightFilter${id}`}
x="-5%"
y="-5%"
width="110%"
height="110%"
>
<feGaussianBlur in="SourceGraphic" stdDeviation="5.5" />
</filter>
<mask id={`glowMask${id}`}>
<rect width="100%" height="100%" fill="transparent" />
<path
d={path}
fill="none"
stroke="url(#strokeWidthGradient)"
strokeWidth="1"
strokeDasharray="50,600"
strokeDashoffset="0"
filter={`url(#glowFilter${id})`}
className="animate-glow mask-path"
/>
<path
d={path}
fill="none"
stroke={`url(#highlight${id})`}
strokeWidth="0.5"
strokeDasharray="50,600"
strokeDashoffset="16"
filter={`url(#highlightFilter${id})`}
className="animate-highlight mask-path"
/>
</mask>
</defs>
<path
d={path}
stroke="#00BEB4"
strokeWidth="1"
fill="none"
opacity="0.1"
/>
<path
d={path}
stroke={`url(#glow${id})`}
strokeWidth="2"
fill="none"
opacity="0.8"
mask={`url(#glowMask${id})`}
/>
</svg>
);
};
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none ">
<div className="absolute top-0 left-0 right-0 w-full">
<div
className={`w-full ml-10`}
style={{ height: aspectRatio['top'] + 'px' }}
>
{def(
'M1282.81 -45L999.839 147.611C988.681 155.206 975.496 159.267 961.999 159.267H746.504H330.429C317.253 159.267 304.368 155.397 293.373 148.137L0.88623 -45',
0,
'top',
)}
</div>
<div
className={`w-full -mt-40`}
style={{ height: aspectRatio['middle'] + 'px' }}
>
{def(
'M0 1L203.392 203.181C215.992 215.705 233.036 222.736 250.802 222.736H287.103C305.94 222.736 323.913 230.636 336.649 244.514L425.401 341.222C438.137 355.1 456.11 363 474.947 363H976.902C996.333 363 1014.81 354.595 1027.59 339.95L1104.79 251.424C1116.14 238.4 1132.08 230.248 1149.29 228.659L1191.13 224.796C1205.62 223.458 1219.28 217.461 1230.06 207.704L1440 17.7981',
1,
'middle',
)}
</div>
<div
className={`w-full -mt-52`}
style={{ height: aspectRatio['bottom'] + 'px' }}
>
{def(
'M-10 1L57.1932 71.1509C67.7929 82.2171 74.2953 96.5714 75.6239 111.837L79.8042 159.87C81.3312 177.416 89.68 193.662 103.057 205.117L399.311 458.829C411.497 469.265 427.011 475 443.054 475H972.606C988.463 475 1003.81 469.396 1015.94 459.179L1310.78 210.75C1323.01 200.451 1331.16 186.136 1333.79 170.369L1341.87 121.837C1344.06 108.691 1350.11 96.492 1359.24 86.7885L1440 1',
2,
'bottom',
)}
</div>
</div>
</div>
);
};

View File

@ -1,267 +0,0 @@
'use client';
import { toast } from '@/components/hooks/use-toast';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/input-otp';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export function SignUpForm() {
const { t } = useTranslate('login');
const FormSchema = z.object({
email: z.string().email({
message: t('emailPlaceholder'),
}),
nickname: z.string({ required_error: t('nicknamePlaceholder') }),
password: z.string({ required_error: t('passwordPlaceholder') }),
agree: z.boolean({ required_error: t('passwordPlaceholder') }),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: '',
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
console.log('🚀 ~ onSubmit ~ data:', data);
toast({
title: 'You submitted the following values:',
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('nicknamePlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type={'password'}
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agree"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
I understand and agree to the Terms of Service and Privacy
Policy.
</FormLabel>
</div>
</FormItem>
)}
/>
<Button type="submit" className="w-full">
{t('signUp')}
</Button>
</form>
</Form>
);
}
export function SignInForm() {
const { t } = useTranslate('login');
const FormSchema = z.object({
email: z.string().email({
message: t('emailPlaceholder'),
}),
password: z.string({ required_error: t('passwordPlaceholder') }),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: '',
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
console.log('🚀 ~ onSubmit ~ data:', data);
toast({
title: 'You submitted the following values:',
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type={'password'}
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('rememberMe')}
</label>
</div>
<Button type="submit" className="w-full">
{t('login')}
</Button>
</form>
</Form>
);
}
export function VerifyEmailForm() {
const FormSchema = z.object({
pin: z.string().min(6, {
message: 'Your one-time password must be 6 characters.',
}),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
pin: '',
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
console.log('🚀 ~ onSubmit ~ data:', data);
toast({
title: 'You submitted the following values:',
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel>One-Time Password</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Verify
</Button>
</form>
</Form>
);
}

View File

@ -1,20 +0,0 @@
import { useCallback } from 'react';
import { useSearchParams } from 'umi';
export enum Step {
SignIn,
SignUp,
ForgotPassword,
ResetPassword,
VerifyEmail,
}
export const useSwitchStep = (step: Step) => {
const [_, setSearchParams] = useSearchParams();
console.log('🚀 ~ useSwitchStep ~ _:', _);
const switchStep = useCallback(() => {
setSearchParams(new URLSearchParams({ step: step.toString() }));
}, [setSearchParams, step]);
return { switchStep };
};

View File

@ -0,0 +1,42 @@
.animate-glow {
animation: glow 16s infinite linear;
}
.mask-path {
stroke-width: 8;
::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
stroke-dasharray: 660;
stroke-dashoffset: 0;
stroke: #d11818;
stroke-width: 8;
fill: none;
}
}
@keyframes glow {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -650;
}
}
@keyframes highlight-flow {
0% {
stroke-dashoffset: 50;
}
100% {
stroke-dashoffset: -600;
} /* 15+300-30=285 */
}
.animate-highlight {
animation: highlight-flow 16s linear infinite;
}
//////////////////////////////////////////////////////////////////////////

View File

@ -1,107 +1,305 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { useTranslate } from '@/hooks/common-hooks';
import { DiscordLogoIcon, GitHubLogoIcon } from '@radix-ui/react-icons';
import { useSearchParams } from 'umi';
import { SignInForm, SignUpForm, VerifyEmailForm } from './form';
import { Step, useSwitchStep } from './hooks';
import SvgIcon from '@/components/svg-icon';
import { useAuth } from '@/hooks/auth-hooks';
import {
useLogin,
useLoginChannels,
useLoginWithChannel,
useRegister,
} from '@/hooks/login-hooks';
import { useSystemConfig } from '@/hooks/system-hooks';
import { rsaPsw } from '@/utils';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
function LoginFooter() {
return (
<section className="pt-4">
<Separator />
<p className="text-center pt-4">or continue with</p>
<div className="flex gap-4 justify-center pt-[20px]">
<GitHubLogoIcon className="w-8 h-8"></GitHubLogoIcon>
<DiscordLogoIcon className="w-8 h-8"></DiscordLogoIcon>
</div>
</section>
);
}
export function SignUpCard() {
const { t } = useTranslate('login');
const { switchStep } = useSwitchStep(Step.SignIn);
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>{t('signUp')}</CardTitle>
</CardHeader>
<CardContent>
<SignUpForm></SignUpForm>
<div className="text-center">
<Button variant={'link'} className="pt-6" onClick={switchStep}>
Already have an account? Log In
</Button>
</div>
<LoginFooter></LoginFooter>
</CardContent>
</Card>
);
}
export function SignInCard() {
const { t } = useTranslate('login');
const { switchStep } = useSwitchStep(Step.SignUp);
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>{t('login')}</CardTitle>
</CardHeader>
<CardContent>
<SignInForm></SignInForm>
<Button
className="w-full mt-2"
onClick={switchStep}
variant={'secondary'}
>
{t('signUp')}
</Button>
</CardContent>
</Card>
);
}
export function VerifyEmailCard() {
// const { t } = useTranslate('login');
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>Verify email</CardTitle>
</CardHeader>
<CardContent>
<section className="flex gap-y-6 flex-col">
<div className="flex items-center space-x-4">
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
Weve sent a 6-digit code to
</p>
<p className="text-sm text-blue-500">yifanwu92@gmail.com.</p>
</div>
<Button>Resend</Button>
</div>
<VerifyEmailForm></VerifyEmailForm>
</section>
</CardContent>
</Card>
);
}
import Spotlight from '@/components/spotlight';
import { Button, ButtonLoading } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { BgSvg } from './bg';
import './index.less';
import { SpotlightTopLeft, SpotlightTopRight } from './spotlight-top';
const Login = () => {
const [searchParams] = useSearchParams();
const step = Number((searchParams.get('step') ?? Step.SignIn) as Step);
const [title, setTitle] = useState('login');
const navigate = useNavigate();
const { login, loading: signLoading } = useLogin();
const { register, loading: registerLoading } = useRegister();
const { channels, loading: channelsLoading } = useLoginChannels();
const { login: loginWithChannel, loading: loginWithChannelLoading } =
useLoginWithChannel();
const { t } = useTranslation('translation', { keyPrefix: 'login' });
const loading =
signLoading ||
registerLoading ||
channelsLoading ||
loginWithChannelLoading;
const { config } = useSystemConfig();
const registerEnabled = config?.registerEnabled !== 0;
const { isLogin } = useAuth();
useEffect(() => {
if (isLogin) {
navigate('/');
}
}, [isLogin, navigate]);
const handleLoginWithChannel = async (channel: string) => {
await loginWithChannel(channel);
};
const changeTitle = () => {
if (title === 'login' && !registerEnabled) {
return;
}
setTitle((title) => (title === 'login' ? 'register' : 'login'));
};
const FormSchema = z
.object({
nickname: z.string().optional(),
email: z
.string()
.email()
.min(1, { message: t('emailPlaceholder') }),
password: z.string().min(1, { message: t('passwordPlaceholder') }),
remember: z.boolean().optional(),
})
.superRefine((data, ctx) => {
if (title === 'register' && !data.nickname) {
ctx.addIssue({
path: ['nickname'],
message: 'nicknamePlaceholder',
code: z.ZodIssueCode.custom,
});
}
});
const form = useForm({
defaultValues: {
nickname: '',
email: '',
password: '',
confirmPassword: '',
remember: false,
},
resolver: zodResolver(FormSchema),
});
const onCheck = async (params) => {
console.log('params', params);
try {
// const params = await form.validateFields();
const rsaPassWord = rsaPsw(params.password) as string;
if (title === 'login') {
const code = await login({
email: `${params.email}`.trim(),
password: rsaPassWord,
});
if (code === 0) {
navigate('/');
}
} else {
const code = await register({
nickname: params.nickname,
email: params.email,
password: rsaPassWord,
});
if (code === 0) {
setTitle('login');
}
}
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
return (
<div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')] bg-cover bg-center">
<div className="inline-block bg-colors-background-neutral-standard rounded-lg">
{step === Step.SignIn && <SignInCard></SignInCard>}
{step === Step.SignUp && <SignUpCard></SignUpCard>}
{step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>}
<div className="min-h-screen relative overflow-hidden">
<BgSvg />
<Spotlight opcity={0.6} coverage={60} />
<SpotlightTopLeft opcity={0.2} coverage={20} />
<SpotlightTopRight opcity={0.2} coverage={20} />
<div className="absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary">
<div className="flex items-center mb-4 w-full pl-10 pt-10 ">
<div className="w-10 h-10 rounded-lg border flex items-center justify-center mr-3">
<img
src={'/logo.svg'}
alt="logo"
className="size-10 mr-[12] cursor-pointer"
/>
</div>
<span className="text-xl font-bold self-end">RAGFlow</span>
</div>
<h1 className="text-2xl font-bold text-center mb-2">
A Leading RAG engine with Agent for superior LLM context.
</h1>
<div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 border border-accent-primary rounded-full hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
Let's get started
</div>
</div>
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 sm:px-6 lg:px-8">
{/* Logo and Header */}
{/* Login Form */}
<div className="text-center mb-8">
<h2 className="text-xl font-semibold text-text-primary">
{title === 'login'
? 'Sign in to Your Account'
: 'Create an Account'}
</h2>
</div>
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-border-button">
<Form {...form}>
<form
className="space-y-6"
onSubmit={form.handleSubmit((data) => onCheck(data))}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...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')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('passwordPlaceholder')}
{...field}
/>
</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>{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"
>
{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-6 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('signUp')}
</Button>
</p>
</div>
)}
{title === 'register' && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('login')}
</Button>
</p>
</div>
)}
</div>
</div>
</div>
);

View File

@ -0,0 +1,70 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import React from 'react';
interface SpotlightProps {
className?: string;
opcity?: number;
coverage?: number;
}
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopLeft: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 10% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopRight: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 90% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};