Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions frontend/components/common/project/BulkImportSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function BulkImportSection({
className="text-xs cursor-pointer hover:bg-gray-300"
onClick={() => onFileUploadOpenChange(true)}
>
TXT导入
文件导入
</Badge>
<Badge variant="secondary" className="bg-muted">
{isEditMode ? '待添加' : '已添加'}: {items.length}个
Expand All @@ -120,7 +120,7 @@ export function BulkImportSection({

<div className="space-y-2">
<Textarea
placeholder={`请输入${placeholderPrefix}分发内容,支持以 逗号分隔(中英文逗号均可)或 每行一个内容 的格式批量导入`}
placeholder={`请输入${placeholderPrefix}分发内容,支持以下格式批量导入:\n• JSON 数组格式:[{}, {}, {}]\n• 每行一个内容\n• 逗号分隔(中英文逗号均可)`}
value={bulkContent}
onChange={(e) => setBulkContent(e.target.value)}
className="h-[100px] break-all overflow-x-auto whitespace-pre-wrap"
Expand Down Expand Up @@ -199,9 +199,9 @@ export function BulkImportSection({
<Dialog open={fileUploadOpen} onOpenChange={onFileUploadOpenChange}>
<DialogContent className={`${isMobile ? 'max-w-[90vw] max-h-[80vh]' : 'max-w-lg'}`}>
<DialogHeader>
<DialogTitle>文件导入分发内容</DialogTitle>
<DialogTitle>文件导入</DialogTitle>
<DialogDescription className="text-xs">
支持 .txt 格式• 每行一个邀请码 • 空行自动忽略 • 大小限制:5MB
支持 .txt 和 .jsonl 格式 • 每行一个内容 • 空行自动忽略 • 大小限制:5MB
</DialogDescription>
</DialogHeader>

Expand Down
31 changes: 29 additions & 2 deletions frontend/components/common/project/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,38 @@ export const getTrustLevelGradient = (trustLevel: number): string => {

/**
* 解析导入的内容文本 - 通用工具函数
* 支持多种格式:
* 1. JSON 数组格式:[{}, {}, {}]
* 2. 每行一个内容
* 3. 逗号分隔格式(向后兼容)
*/
export const parseImportContent = (content: string): string[] => {
let parsed = content.split('\n').filter((item) => item.trim());
const trimmedContent = content.trim();

// 尝试解析为 JSON 数组
if (trimmedContent.startsWith('[') && trimmedContent.endsWith(']')) {
try {
const jsonArray = JSON.parse(trimmedContent);
if (Array.isArray(jsonArray)) {
return jsonArray
.map((item) => {
if (typeof item === 'object' && item !== null) {
return JSON.stringify(item);
}
return String(item);
})
.filter((item) => item.trim())
.map((item) => item.substring(0, FORM_LIMITS.CONTENT_ITEM_MAX_LENGTH));
}
} catch {
// JSON 解析失败,继续使用原有逻辑
}
}

// 原有逻辑:按行分割,如果只有一行则按逗号分割
let parsed = trimmedContent.split('\n').filter((item) => item.trim());
if (parsed.length === 1) {
parsed = content
parsed = trimmedContent
.replace(/,/g, ',')
.split(',')
.filter((item) => item.trim());
Expand Down
94 changes: 83 additions & 11 deletions frontend/hooks/use-file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,62 @@ import {useState, useCallback} from 'react';
import {toast} from 'sonner';
import {handleBulkImportContentWithFilter} from '@/components/common/project';

/**
* 解析 JSONL 文件内容
* 支持两种格式:
* 1. JSON 数组格式:[{}, {}, {}]
* 2. 每行一个 JSON 对象
*/
const parseJsonlContent = (content: string): string => {
const trimmedContent = content.trim();

// 尝试解析为 JSON 数组格式
if (trimmedContent.startsWith('[') && trimmedContent.endsWith(']')) {
try {
const jsonArray = JSON.parse(trimmedContent);
if (Array.isArray(jsonArray)) {
return jsonArray
.map((item) => {
if (typeof item === 'object' && item !== null) {
return JSON.stringify(item);
}
return String(item);
})
.filter((item) => item.trim())
.join('\n');
}
} catch {
// JSON 数组解析失败,继续尝试逐行解析
}
}

// 逐行解析 JSON 对象
const lines = trimmedContent
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);

const validJsonLines: string[] = [];

for (const line of lines) {
try {
// 尝试解析为 JSON 对象
const jsonObj = JSON.parse(line);
if (typeof jsonObj === 'object' && jsonObj !== null) {
validJsonLines.push(JSON.stringify(jsonObj));
} else {
// 非对象类型,直接转为字符串
validJsonLines.push(String(jsonObj));
}
} catch {
// JSON 解析失败,作为普通文本处理
validJsonLines.push(line);
}
}

return validJsonLines.join('\n');
};

export function useFileUpload() {
const [fileUploadOpen, setFileUploadOpen] = useState(false);

Expand All @@ -16,10 +72,11 @@ export function useFileUpload() {
if (files.length === 0) return;

const file = files[0];
const fileName = file.name.toLowerCase();

// 检查文件类型
if (!file.name.toLowerCase().endsWith('.txt')) {
toast.error('仅支持上传 .txt 格式的文件');
if (!fileName.endsWith('.txt') && !fileName.endsWith('.jsonl')) {
toast.error('仅支持上传 .txt 或 .jsonl 格式的文件');
return;
}

Expand All @@ -33,20 +90,35 @@ export function useFileUpload() {
reader.onload = (e) => {
const content = e.target?.result as string;
if (content) {
// 按行分割并过滤空行
const lines = content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);

if (lines.length === 0) {
toast.error('文件内容为空');
let processedContent = content;

// 根据文件扩展名选择解析方式
if (fileName.endsWith('.jsonl')) {
// JSONL 文件处理
processedContent = parseJsonlContent(content);
} else {
// TXT 文件处理(保持原有逻辑)
const lines = content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);

if (lines.length === 0) {
toast.error('文件内容为空');
return;
}

processedContent = lines.join('\n');
}

if (!processedContent) {
toast.error('文件内容为空或格式无效');
return;
}

// 执行导入
handleBulkImportContentWithFilter(
lines.join('\n'),
processedContent,
currentItems,
allowDuplicates,
(updatedItems: string[], importedCount: number, skippedInfo?: string) => {
Expand Down